How compatible are the "builder" design pattern and Spring dependency injection? Consider this code
@Test
@Sql(executionPhase = BEFORE_TEST_METHOD, value = BASE_SCRIPT_PATH + "GetCommentTest/before.sql") // inserting sample rows
@Sql(executionPhase = AFTER_TEST_METHOD, value = BASE_SCRIPT_PATH + "GetCommentTest/after.sql") // truncating
private void getPageOneDefaultTest() throws Exception {
MockHttpServletResponse response = mockMvc.perform(get(BASE_URI + "page/" + 1)
.header(HttpHeaders.AUTHORIZATION, token))
.andExpect(status().isOk())
.andReturn()
.getResponse();
/*
The whole idea with "expectation testers" may look a bit unusual, but if you consider that
I also have methods getPageOneSizeFiveTest(), getPageTwoSizeFiveTest() (probably, I
should add more of them for a better coverage), you should realize it removes a lot of
code duplication
*/
expectationTester = new GetCommentPageExpectationTester.Builder(response)
.setExpectedPageCount(10)
.setExpectedPageDtoListSize(10)
.setExpectedOwnerUsername("mickey_m")
.build();
expectationTester.test();
}
public class GetCommentPageExpectationTester implements ExpectationTester {
private final String serializedResponseBody;
private final int expectedPageCount;
private final int expectedPageDtoListSize;
private final int expectedFirstCommentId;
private final String expectedCommentText;
private final int expectedQuestionId;
private final int expectedOwnerId;
private final String expectedOwnerUsername;
private final ObjectMapper objectMapper;
private GetCommentPageExpectationTester(Builder builder) {
this.serializedResponseBody = builder.serializedResponseBody;
this.expectedPageCount = builder.expectedPageCount;
this.expectedPageDtoListSize = builder.expectedPageDtoListSize;
this.expectedFirstCommentId = builder.expectedFirstCommentId;
this.expectedCommentText = builder.expectedCommentText;
this.expectedQuestionId = builder.expectedQuestionId;
this.expectedOwnerId = builder.expectedOwnerId;
this.expectedOwnerUsername = builder.expectedOwnerUsername;
this.objectMapper = new ObjectMapper();
}
@Override
public void test() throws JsonProcessingException {
Data<Page<QuestionCommentResponseDto>> deserializedResponseBody =
objectMapper.readValue(serializedResponseBody, Data.class); // once I post it on CodeReview, you may comment on this unchecked cast
assertNotNull(deserializedResponseBody.getData());
Page<QuestionCommentResponseDto> page = deserializedResponseBody.getData();
assertEquals(expectedPageCount, page.getCount());
assertNotNull(page.getDtos());
List<QuestionCommentResponseDto> dtoList = page.getDtos();
assertEquals(expectedPageDtoListSize, dtoList.size());
QuestionCommentResponseDto dto;
for (int i = 1; i <= dtoList.size(); i++) {
dto = dtoList.get(i);
assertEquals(expectedFirstCommentId, dto.getId());
assertEquals(expectedQuestionId, dto.getQuestionId());
assertEquals(expectedCommentText, dto.getText());
assertNotNull(dto.getCreatedDate());
assertNotNull(dto.getModifiedDate());
AccountResponseDto actualOwner = dto.getOwner();
assertEquals(expectedOwnerId, actualOwner.getId());
assertEquals(expectedOwnerUsername, actualOwner.getUsername());
}
}
public static class Builder {
private final String serializedResponseBody;
private int expectedPageCount;
private int expectedPageDtoListSize;
private int expectedFirstCommentId = 1;
private String expectedCommentText = "text";
private int expectedQuestionId = 1;
private int expectedOwnerId = 1;
private String expectedOwnerUsername;
public Builder(MockHttpServletResponse response) throws UnsupportedEncodingException {
this.serializedResponseBody = response.getContentAsString();
}
public Builder setExpectedPageCount(int expectedPageCount) {
this.expectedPageCount = expectedPageCount;
return this;
}
public Builder setExpectedPageDtoListSize(int expectedPageDtoListSize) {
this.expectedPageDtoListSize = expectedPageDtoListSize;
return this;
}
// the rest of the setters are omitted
public GetCommentPageExpectationTester build() {
return new GetCommentPageExpectationTester(this);
}
}
}
When I call build()
, it invokes a private constructor and returns the instance of the top-level class copying the field values of the Builder
. Now, suppose I want ObjectMapper
autowired. I annotate both the top-level class and the Builder
as @Component
s. Then if I call build()
, ObjectMapper
isn't going to be injected, is it?, since the top-level class instance is going to be created with a simple constructor call, it's not coming from the Spring container, is it? Can I have a builder like that while also autowiring dependencies? While in this case I can simply initialize the ObjectMapper
field with a no-args constructor (hoping it's going to be the same ObjectMapper
as the autowired one), it may make sense for "expectation testers" that use an EntityManager
// most fields are omitted for brevity
@Component
public class GetCommentExpectationTester implements ExpectationTester {
@PersistenceContext
private EntityManager entityManager;
@Override
public void test() {
assertTrue(entityManager.createQuery("""
SELECT COUNT(qc.id) = 1
FROM QuestionComment qc
JOIN qc.owner ow
JOIN qc.question q
WHERE qc.createdDate IS NOT NULL
AND qc.modifiedDate IS NOT NULL
AND qc.text = 'text'
AND ow.id = 1
AND q.id = 1
""", Boolean.class)
.getSingleResult());
}
I can autowire a field, an ObjectMapper
or an EntityManager
for example, at the Builder
level. But then I won't be able to create a Builder
instance and pass a response right in a test method like that, will I?
I can also simply pass the autowired EntityManager
as a method argument (the test class has one) when creating a Builder
instance. But since reading Robert Martin's books, I am super-wary of passing anything. The less you pass, the better, that's my takeaway
Aucun commentaire:
Enregistrer un commentaire