🌱TDD: ν…ŒμŠ€νŠΈ λ²”μœ„μ™€ μ’…λ₯˜ & ν…ŒμŠ€νŠΈ μ½”λ“œμ™€ μœ μ§€λ³΄μˆ˜

HyojinΒ·2024λ…„ 8μ›” 31일
0

TDD

λͺ©λ‘ 보기
4/4

✨ ν…ŒμŠ€νŠΈ λ²”μœ„μ™€ μ’…λ₯˜

ν…ŒμŠ€νŠΈ λ²”μœ„

  • λ‹¨μœ„ ν…ŒμŠ€νŠΈ
  • 톡합 ν…ŒμŠ€νŠΈ
  • κΈ°λŠ₯ ν…ŒμŠ€νŠΈ

κΈ°λŠ₯ ν…ŒμŠ€νŠΈμ™€ E2E ν…ŒμŠ€νŠΈ

  • κΈ°λŠ₯ ν…ŒμŠ€νŠΈλŠ” μ‚¬μš©μž μž…μž₯μ—μ„œ μ‹œμŠ€ν…œμ΄ μ œκ³΅ν•˜λŠ” κΈ°λŠ₯이 μ˜¬λ°”λ₯΄κ²Œ λ™μž‘ν•˜λŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€.
  • κΈ°λŠ₯ ν…ŒμŠ€νŠΈλŠ” E2E(End-to-End) ν…ŒμŠ€νŠΈλ‘œ λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€.
  • 주둜 QA μ‘°μ§μ—μ„œ κΈ°λŠ₯ ν…ŒμŠ€νŠΈλ₯Ό μˆ˜ν–‰ν•©λ‹ˆλ‹€.

톡합 ν…ŒμŠ€νŠΈ

  • μ‹œμŠ€ν…œμ˜ 각 ꡬ성 μš”μ†Œκ°€ μ˜¬λ°”λ₯΄κ²Œ μ—°λ™λ˜μ—ˆλŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€.
  • μ†Œν”„νŠΈμ›¨μ–΄ μ½”λ“œλ₯Ό 직접 ν…ŒμŠ€νŠΈν•©λ‹ˆλ‹€.
  • 예λ₯Ό λ“€μ–΄, μŠ€ν”„λ§ ν”„λ ˆμž„μ›Œν¬, JPA, MariaDBλ₯Ό μ΄μš©ν•˜μ—¬ νšŒμ› κ°€μž… κ΄€λ ¨ μ„œλΉ„μŠ€ 클래슀, DAO μΈν„°νŽ˜μ΄μŠ€, SQL 쿼리λ₯Ό κ΅¬ν˜„ν•œ 경우, 이듀을 ν†΅ν•©ν•œ νšŒμ› κ°€μž… μ„œλΉ„μŠ€ ν΄λž˜μŠ€μ— λŒ€ν•œ ν…ŒμŠ€νŠΈκ°€ 톡합 ν…ŒμŠ€νŠΈμ˜ μ˜ˆκ°€ 될 수 μžˆμŠ΅λ‹ˆλ‹€.

λ‹¨μœ„ ν…ŒμŠ€νŠΈ

  • κ°œλ³„ μ½”λ“œλ‚˜ μ»΄ν¬λ„ŒνŠΈκ°€ κΈ°λŒ€ν•œ λŒ€λ‘œ λ™μž‘ν•˜λŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€.
  • 주둜 ν•œ ν΄λž˜μŠ€λ‚˜ ν•œ λ©”μ„œλ“œμ™€ 같은 μž‘μ€ λ²”μœ„λ₯Ό ν…ŒμŠ€νŠΈν•©λ‹ˆλ‹€.

ν…ŒμŠ€νŠΈ λ²”μœ„ κ°„μ˜ 차이

  • 톡합 ν…ŒμŠ€νŠΈλ₯Ό μ‹€ν–‰ν•˜λ €λ©΄ DBλ‚˜ μΊμ‹œ μ„œλ²„μ™€ 같은 연동 λŒ€μƒμ„ ꡬ성해야 ν•©λ‹ˆλ‹€.
  • κΈ°λŠ₯ ν…ŒμŠ€νŠΈλ₯Ό μ‹€ν–‰ν•˜λ €λ©΄ μ›Ή μ„œλ²„λ₯Ό κ΅¬λ™ν•˜κ±°λ‚˜ λͺ¨λ°”일 앱을 μ„€μΉ˜ν•΄μ•Ό ν•©λ‹ˆλ‹€.
  • 톡합 ν…ŒμŠ€νŠΈλŠ” DB 연동, μ†ŒμΌ“ 톡신, μŠ€ν”„λ§ μ»¨ν…Œμ΄λ„ˆ μ΄ˆκΈ°ν™” λ“±μœΌλ‘œ 인해 ν…ŒμŠ€νŠΈ μ‹€ν–‰ 속도가 느렀질 수 μžˆμŠ΅λ‹ˆλ‹€.
  • 톡합 ν…ŒμŠ€νŠΈλ‚˜ κΈ°λŠ₯ ν…ŒμŠ€νŠΈλŠ” μ™ΈλΆ€ 상황에 μ˜ν•΄ μ€€λΉ„λ‚˜ ν…ŒμŠ€νŠΈκ°€ μ–΄λ €μšΈ 수 μžˆμŠ΅λ‹ˆλ‹€.
    톡합 ν…ŒμŠ€νŠΈλŠ” λ§Žμ€ μ€€λΉ„κ°€ ν•„μš”ν•˜μ§€λ§Œ, ν•„μˆ˜μ μž…λ‹ˆλ‹€. λ‹¨μœ„ ν…ŒμŠ€νŠΈλ₯Ό 아무리 많이 μž‘μ„±ν•΄λ„ 각 ꡬ성 μš”μ†Œκ°€ μ˜¬λ°”λ₯΄κ²Œ μ—°λ™λ˜λŠ”μ§€λŠ” λ°˜λ“œμ‹œ 확인해야 ν•©λ‹ˆλ‹€.

ν…ŒμŠ€νŠΈ λ²”μœ„μ— λ”°λ₯Έ ν…ŒμŠ€νŠΈ μ½”λ“œ κ°œμˆ˜μ™€ μ‹œκ°„

ν…ŒμŠ€νŠΈλ₯Ό μžλ™ν™”ν• μˆ˜λ‘ κ³ ν’ˆμ§ˆ μ†Œν”„νŠΈμ›¨μ–΄λ₯Ό 더 λΉ λ₯΄κ²Œ μΆœμ‹œν•  수 μžˆμŠ΅λ‹ˆλ‹€.
ν…ŒμŠ€νŠΈ μ†λ„λŠ” 톡합 ν…ŒμŠ€νŠΈλ³΄λ‹€ λ‹¨μœ„ ν…ŒμŠ€νŠΈκ°€ λΉ λ₯΄λ―€λ‘œ, κ°€λŠ₯ν•œ ν•œ λ‹¨μœ„ ν…ŒμŠ€νŠΈλ‘œ λ‹€μ–‘ν•œ 상황을 닀루고, 톡합 ν…ŒμŠ€νŠΈλ‚˜ κΈ°λŠ₯ ν…ŒμŠ€νŠΈλŠ” μ£Όμš” 상황에 μ΄ˆμ μ„ λ§žμΆ”λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€.

μ™ΈλΆ€ 연동이 ν•„μš”ν•œ ν…ŒμŠ€νŠΈ

WireMock

  • μ„œλ²„ APIλ₯Ό μŠ€ν…μœΌλ‘œ λŒ€μ²΄ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • JSON/XML 응닡, HTTPS 지원, 단독 μ‹€ν–‰ λ“± λ‹€μ–‘ν•œ κΈ°λŠ₯을 μ œκ³΅ν•©λ‹ˆλ‹€.
μ½”λ“œ 볡사
public class CardNumberValidatorTest {

    private WireMockServer wireMockServer;

    @BeforeEach
    void setUp() {
        wireMockServer = new WireMockServer(options().port(8089));
        wireMockServer.start();
    }

    @AfterEach
    void tearDown() {
        wireMockServer.stop();
    }

    @Test
    void valid() {
        wireMockServer.stubFor(post(urlEqualTo("/card"))
                .withRequestBody(equalTo("1234567890"))
                .willReturn(aResponse()
                        .withHeader("Content-Type", "text/plain")
                        .withBody("ok"))
        );

        CardNumberValidator validator =
                new CardNumberValidator("http://localhost:8089");
        CardValidity validity = validator.validate("1234567890");
        assertEquals(CardValidity.VALID, validity);
    }

    @Test
    void timeout() {
        wireMockServer.stubFor(post(urlEqualTo("/card"))
                .willReturn(aResponse()
                        .withFixedDelay(5000))
        );

        CardNumberValidator validator =
                new CardNumberValidator("http://localhost:8089");
        CardValidity validity = validator.validate("1234567890");
        assertEquals(CardValidity.TIMEOUT, validity);
    }
}

μŠ€ν”„λ§ λΆ€νŠΈμ˜ λ‚΄μž₯ μ„œλ²„λ₯Ό μ΄μš©ν•œ API κΈ°λŠ₯ ν…ŒμŠ€νŠΈ

  • μŠ€ν”„λ§ λΆ€νŠΈλ₯Ό μ‚¬μš©ν•˜λ©΄ λ‚΄μž₯ 톰캣을 μ΄μš©ν•΄ API에 λŒ€ν•œ ν…ŒμŠ€νŠΈλ₯Ό JUnit μ½”λ“œλ‘œ μž‘μ„±ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
μ½”λ“œ 볡사
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserApiE2ETest {
    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void weakPwResponse() {
        String reqBody = "{\"id\": \"id\", \"pw\": \"123\", \"email\": \"a@a.com\" }";
        RequestEntity<String> request =
                RequestEntity.post(URI.create("/users"))
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .body(reqBody);

        ResponseEntity<String> response = restTemplate.exchange(
                request,
                String.class);

        assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
        assertTrue(response.getBody().contains("WeakPasswordException"));
    }
}

`

✨ ν…ŒμŠ€νŠΈ μ½”λ“œμ™€ μœ μ§€λ³΄μˆ˜

ν…ŒμŠ€νŠΈ 주도 κ°œλ°œμ„ 톡해 μž‘μ„±λœ ν…ŒμŠ€νŠΈ μ½”λ“œλŠ” CI/CD νŒŒμ΄ν”„λΌμΈμ—μ„œ μžλ™ν™” ν…ŒμŠ€νŠΈλ‘œ μ‚¬μš©λ©λ‹ˆλ‹€. 이λ₯Ό 톡해 배포 전에 버그λ₯Ό λ°œκ²¬ν•˜κ³  λ°©μ§€ν•˜μ—¬ μ†Œν”„νŠΈμ›¨μ–΄ ν’ˆμ§ˆμ΄ μ €ν•˜λ˜λŠ” 것을 막을 수 μžˆμŠ΅λ‹ˆλ‹€. κ·ΈλŸ¬λ‚˜ ν…ŒμŠ€νŠΈ μ½”λ“œλ„ μ œν’ˆ μ½”λ“œμ™€ λ§ˆμ°¬κ°€μ§€λ‘œ 지속적인 μœ μ§€λ³΄μˆ˜κ°€ ν•„μš”ν•©λ‹ˆλ‹€. ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό λ°©μΉ˜ν•˜λ©΄ μ—¬λŸ¬ λ¬Έμ œκ°€ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€.

ν…ŒμŠ€νŠΈ μ½”λ“œ 방치의 문제점

νšŒκ·€ ν…ŒμŠ€νŠΈμ˜ 신뒰도 μ €ν•˜

μ½”λ“œλ₯Ό λ³€κ²½ν•  λ•Œ κΈ°μ‘΄ κΈ°λŠ₯이 μ˜¬λ°”λ₯΄κ²Œ λ™μž‘ν•˜λŠ”μ§€ ν™•μΈν•˜λŠ” νšŒκ·€ ν…ŒμŠ€νŠΈλ₯Ό μžλ™ν™”ν•©λ‹ˆλ‹€. ν•˜μ§€λ§Œ κΉ¨μ§„ ν…ŒμŠ€νŠΈλ₯Ό λ°©μΉ˜ν•˜λ©΄ νšŒκ·€ ν…ŒμŠ€νŠΈμ˜ 검증 λ²”μœ„κ°€ 쀄어듀어 μƒˆλ‘œμš΄ λ³€κ²½ 사항이 κΈ°μ‘΄ κΈ°λŠ₯에 λ―ΈμΉ˜λŠ” 영ν–₯을 μ •ν™•νžˆ νŒŒμ•…ν•˜κΈ° μ–΄λ €μ›Œμ§‘λ‹ˆλ‹€.

ν…ŒμŠ€νŠΈ 톡과λ₯Ό μœ„ν•œ λΆˆν•„μš”ν•œ λ…Έλ ₯ 증가

κΉ¨μ§„ ν…ŒμŠ€νŠΈκ°€ λ§Žμ•„μ§€λ©΄, μ΄λŸ¬ν•œ ν…ŒμŠ€νŠΈλ₯Ό ν†΅κ³Όμ‹œν‚€κΈ° μœ„ν•΄ λ§Žμ€ λ…Έλ ₯이 ν•„μš”ν•΄μ§‘λ‹ˆλ‹€. μ΄λŠ” TDD의 본래 λͺ©μ μ—μ„œ λ²—μ–΄λ‚  수 μžˆμŠ΅λ‹ˆλ‹€.

μ΄λŸ¬ν•œ μ•…μˆœν™˜μ„ λ°©μ§€ν•˜λ €λ©΄ ν…ŒμŠ€νŠΈ μ½”λ“œμ˜ μœ μ§€λ³΄μˆ˜μ„±μ΄ λ†’μ•„μ•Ό ν•©λ‹ˆλ‹€.

κΉ¨μ§„ 유리창 이둠

"κΉ¨μ§„ 유리창 이둠"은 μ‚¬μ†Œν•œ λ¬΄μ§ˆμ„œλ₯Ό λ°©μΉ˜ν•˜λ©΄ 더 큰 문제둜 μ΄μ–΄μ§ˆ κ°€λŠ₯성이 λ†’μ•„μ§„λ‹€λŠ” μ΄λ‘ μž…λ‹ˆλ‹€. μ΄λŠ” ν…ŒμŠ€νŠΈ μ½”λ“œμ—λ„ μ μš©λ©λ‹ˆλ‹€. ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό λ°©μΉ˜ν•˜κΈ° μ‹œμž‘ν•˜λ©΄ μ‹€νŒ¨ν•˜λŠ” ν…ŒμŠ€νŠΈκ°€ 점점 λ§Žμ•„μ Έ, κ²°κ΅­μ—λŠ” ν…ŒμŠ€νŠΈλ₯Ό μœ μ§€λ³΄μˆ˜ν•  수 없을 μ •λ„λ‘œ 악화될 수 μžˆμŠ΅λ‹ˆλ‹€.

ν…ŒμŠ€νŠΈ μ½”λ“œμ˜ μœ μ§€λ³΄μˆ˜λ₯Ό μœ„ν•œ 방법

λ³€μˆ˜λ‚˜ ν•„λ“œλ₯Ό μ‚¬μš©ν•΄ κΈ°λŒ“κ°’μ„ ν‘œν˜„ν•˜μ§€ μ•ŠκΈ°

@Test
void dateFormat() {
	LocalDate date = LocalDate.of(1945,8,15);
	String dataStr = formatDate(date);
	assertEquals(date.getYear()+"λ…„ "+
			date.getMonthValue()+"μ›” "+
			date.getDayOfMonth()+"일 ", dateStr);
}

ν…ŒμŠ€νŠΈ μ½”λ“œμ—μ„œ 객체λ₯Ό 생성할 λ•Œ μ‚¬μš©ν•œ 값을 ν™•μΈν•˜κΈ° μœ„ν•΄ ν•„λ“œλ‚˜ λ³€μˆ˜λ₯Ό μ°Έμ‘°ν•˜μ§€ μ•Šλ„λ‘ ν•΄μ•Ό ν•©λ‹ˆλ‹€.

@Test
void dateFormat() {
	LocalDate date = LocalDate.of(1945,8,15);
	String dataStr = formatDate(date);
	assertEquals("1945λ…„ 8μ›” 15일", dateStr);
}

μ΄λ ‡κ²Œ ν•˜λ©΄ ν…ŒμŠ€νŠΈ μ½”λ“œκ°€ 더 λͺ…ν™•ν•΄μ§€κ³  μœ μ§€λ³΄μˆ˜ν•˜κΈ° μ‰¬μ›Œμ§‘λ‹ˆλ‹€.

private List<Integer> answers = Arrays.asList(1,2,3,4);
private Long respondentId = 100L;

@DisplayName("닡변에 μ„±κ³΅ν•˜λ©΄ κ²°κ³Ό μ €μž₯함")
@Test
public void saveAnswerSuccessfully() {
	// λ‹΅λ³€ν•  섀문이 쑴재
	Survey survey = SurveyFactory.createApprovedSurvey(1);
	surveyRepository.save(survey);

	// μ„€λ¬Έ λ‹΅λ³€
	SurveyAnswerRequest surveyAnswer = SurveyAnswerRequest.builder()
		.surveyId(survey.getId())
		.respondentId(respondentId)
		.answers(answers)
		.build();

	svc.answerSurvey(surveyAnswer);

	// μ €μž₯ κ²°κ³Ό 확인
	SurveyAnswer savedAnswer =
		memoryRepository.findBySurveyAndRespondent(
			survey.getId(), respondentId);
	assertAll(
		()->assertEquals(respondentId, savedAnswer.getRespondentId()),
		()->assertEquals(answer.size(), savedAnswer.getAnswers().size()),
		()->assertEquals(answer.get(0), savedAnswer.getAnswers().get(0)),
		()->assertEquals(answer.get(1), savedAnswer.getAnswers().get(1)),
		()->assertEquals(answer.get(2), savedAnswer.getAnswers().get(2)),
		()->assertEquals(answer.get(3), savedAnswer.getAnswers().get(3))
		);
}

단언과 객체 생성에 ν•„λ“œμ™€ λ³€μˆ˜λ₯Ό μ‚¬μš©ν• κ²½μš°, ν•„λ“œμ˜ 값을 κ²€μ¦ν•˜λŠ” κ³Όμ •μ—μ„œ μ‹€νŒ¨ν•˜λ©΄ ν•΄λ‹Ή ν•„λ“œλ₯Ό 확인해야 ν•œλ‹€.
λ˜λŠ” ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό 처음 λ³΄λŠ” μ‚¬λžŒμ€ λ³€μˆ˜μ™€ ν•„λ“œλ₯Ό μ˜€κ°€λ©° ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό 이해해야 ν•œλ‹€.

@DisplayName("닡변에 μ„±κ³΅ν•˜λ©΄ κ²°κ³Ό μ €μž₯함")
@Test
public void saveAnswerSuccessfully() {
	// λ‹΅λ³€ν•  섀문이 쑴재
	Survey survey = SurveyFactory.createApprovedSurvey(1);
	surveyRepository.save(survey);

	// μ„€λ¬Έ λ‹΅λ³€
	SurveyAnswerRequest surveyAnswer = SurveyAnswerRequest.builder()
		.surveyId(1L)
		.respondentId(100L)
		.answers(Arrays.asList(1,2,3,4))
		.build();

	svc.answerSurvey(surveyAnswer);

	// μ €μž₯ κ²°κ³Ό 확인
	SurveyAnswer savedAnswer =
		memoryRepository.findBySurveyAndRespondent(
			survey.getId(), respondentId);
	assertAll(
		()->assertEquals(100L, savedAnswer.getRespondentId()),
		()->assertEquals(4, savedAnswer.getAnswers().size()),
		()->assertEquals(1, savedAnswer.getAnswers().get(0)),
		()->assertEquals(2, savedAnswer.getAnswers().get(1)),
		()->assertEquals(3, savedAnswer.getAnswers().get(2)),
		()->assertEquals(4, savedAnswer.getAnswers().get(3))
		);
}

객체 생성과 λ‹¨μ–Έμ—μ„œ λ³€μˆ˜ λŒ€μ‹  값을 μ‚¬μš©ν•˜λ©΄ μ½”λ“œ 가독성이 μ’‹μ•„μ Έμ„œ ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό μ‰½κ²Œ νŒŒμ•…ν•  수 μžˆλ‹€.

ν•œ ν…ŒμŠ€νŠΈμ—μ„œ 두 κ°€μ§€ μ΄μƒμ˜ 사항을 κ²€μ¦ν•˜μ§€ μ•ŠκΈ°

@DisplayName("같은 IDκ°€ μ—†μœΌλ©΄ κ°€μž…μ— μ„±κ³΅ν•˜κ³  메일을 전솑함")
@Test
void registerAndSendEmail() {
	userRegister.register("id", "pw", "email");
	
	// 검증1 : νšŒμ› 데이터가 μ˜¬λ°”λ₯΄κ²Œ μ €μž₯λ˜μ—ˆλŠ”μ§€ 검증
	User savedUser = userRepository.findById("id");
	assertEquals("id", savedUser.getId());
	assertEquals("email", savedUser.getEmail());

	// 검증2 : 이메일 λ°œμ†‘μ„ μš”μ²­ν–ˆλŠ”μ§€ 검증
	ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
	BDDMockito.then(mockEmailNotifier)
		.should().sendRegisterEmail(captor.capture());

	String realEmail = captor.getValue();
	assertEquals("email@email.com", realEmail);
}

ν•œ ν…ŒμŠ€νŠΈ λ©”μ„œλ“œμ—μ„œ μ„œλ‘œ λ‹€λ₯Έ λ‚΄μš©μ„ κ²€μ¦ν•˜λŠ” 것은 μ§€μ–‘ν•΄μ•Ό ν•©λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, νšŒμ› 데이터 μ €μž₯ 여뢀와 이메일 λ°œμ†‘ μš”μ²­ μ—¬λΆ€λ₯Ό λ™μ‹œμ— κ²€μ¦ν•˜λŠ” λŒ€μ‹ , 각각의 λ‚΄μš©μ„ λ³„λ„μ˜ ν…ŒμŠ€νŠΈλ‘œ 뢄리해 집쀑도λ₯Ό λ†’μ—¬μ•Ό ν•©λ‹ˆλ‹€.

@DisplayName("같은 IDκ°€ μ—†μœΌλ©΄ κ°€μž… 성곡함")
@Test
void noDupId_RegisterSuccess() {
	userRegister.register("id", "pw", "email");
	
	User savedUser = fakeRepository.findById("id");
	assertEquals("id", savedUser.getId());
	assertEquals("email", savedUser.getEmail());
}

@DisplayName("κ°€μž…ν•˜λ©΄ 메일을 전솑함")
@Test
void whenRegisterThenSendMail() {
	userRegister.register("id", "pw", "email@email.com");

	ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
	then(mockEmailNotifier).should().sendRegisterEmail(captor.capture());

	String realEmail = captor.getValue();
	assertEquals("email@email.com", realEmail);
}

μ •ν™•ν•˜κ²Œ μΌμΉ˜ν•˜λŠ” κ°’μœΌλ‘œ λͺ¨μ˜ 객체 μ„€μ •ν•˜μ§€ μ•ŠκΈ°

@DisplayName("μ•½ν•œ μ•”ν˜Έλ©΄ κ°€μž… μ‹€νŒ¨")
@Test
void weakPassword() {
	BDDMockito.given(mockPasswordChecker.checkPasswordWeak("pw"))
		.willReturn(true);

	assertThrows(WeakPasswordException.class, ()->{
		userRegister.register("id", "pw", "email");
	});
}

μ •ν™•νžˆ μΌμΉ˜ν•˜λŠ” 상황을 μ •μ˜ν•œ λͺ¨μ˜(Mock) κ°μ²΄λŠ” μž‘μ€ 변화에도 ν…ŒμŠ€νŠΈκ°€ μ‹€νŒ¨ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
μ•½ν•œ μ•”ν˜Έ ν…ŒμŠ€νŠΈλŠ” UserRegister κ°€ μ›ν•˜λŠ” λŒ€λ‘œ λ™μž‘ν•˜λŠ” μ§€ ν™•μΈν•˜κΈ° μœ„ν•œ μ½”λ“œμž…λ‹ˆλ‹€.
이럴 경우 μž„μ˜μ˜ 값에 μΌμΉ˜ν•˜λ„λ‘ λͺ¨μ˜ 객체λ₯Ό μ„€μ •ν•˜λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, Mockito.anyString()을 μ‚¬μš©ν•΄ μž„μ˜μ˜ λ¬Έμžμ—΄κ³Ό μΌμΉ˜ν•˜λ„λ‘ μ„€μ •ν•©λ‹ˆλ‹€.

@DisplayName("μ•½ν•œ μ•”ν˜Έλ©΄ κ°€μž… μ‹€νŒ¨")
@Test
void weakPassword() {
	BDDMockito.given(mockPasswordChecker.checkPasswordWeak(Mockito.anyString()))
		.willReturn(true);

	assertThrows(WeakPasswordException.class, ()->{
		userRegister.register("id", "pw", "email");
	});
}

λͺ¨μ˜ 객체λ₯Ό ν˜ΈμΆœν–ˆλŠ”μ§€ μ—¬λΆ€λ₯Ό ν™•μΈν•˜λŠ” κ²½μš°λ„ λ™μΌν•©λ‹ˆλ‹€.

@DisplayName("νšŒμ› κ°€μž…μ‹œ μ•”ν˜Έ 검사 μˆ˜ν–‰λ¨")
@Test
void checkPassword() {
	userRegister.register("id", "pw", "email");

	BDDMockito.then(mockPasswordChecker)
		.should()
		.checkPasswordWeak("pw");
}

κ³Όλ„ν•˜κ²Œ λ‚΄λΆ€ κ΅¬ν˜„μ„ κ²€μ¦ν•˜μ§€ μ•ŠκΈ°

@DisplayName("νšŒμ› κ°€μž…μ‹œ μ•”ν˜Έ 검사 μˆ˜ν–‰λ¨")
@Test
void checkPassword() {
	userRegister.register("id", "pw", "email");
	
	// PasswordChecker#checkPasswordWeak() λ©”μ„œλ“œ 호좜 μ—¬λΆ€ 검사
	BDDMockito.then(mockPasswordChecker)
		.should()
		.checkPasswordWeak(Mockito.anyString());

	// UserRepository#findById() λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•˜μ§€ μ•ŠλŠ” 것을 검사
	BDDMockito.then(mockRepository)
		.should(Mockito.never())
		.findById(Mockito.anyString());
}

λ‚΄λΆ€ κ΅¬ν˜„μ„ κ²€μ¦ν•˜λŠ” 것은 쒋은 방법이 μ•„λ‹™λ‹ˆλ‹€. λ‚΄λΆ€ κ΅¬ν˜„μ€ μ–Έμ œλ“ μ§€ λ°”λ€” 수 μžˆμœΌλ―€λ‘œ, μ‹€ν–‰ κ²°κ³Όλ₯Ό κ²€μ¦ν•˜λŠ” 데 집쀑해야 ν•©λ‹ˆλ‹€.

예제 μ½”λ“œμ˜ 경우 register() λ©”μ„œλ“œκ°€ PasswordChecker#checkPasswordWeak() λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•˜λŠ”μ§€ κ²€μ¦ν•˜λŠ” 것보닀 μ•½ν•œ μ•”ν˜ΈμΌ λ•Œ register의 κ²°κ³Όκ°€ μ˜¬λ°”λ₯Έμ§€ κ²€μ¦ν•΄μ•Όν•©λ‹ˆλ‹€.

이미 μ‘΄μž¬ν•˜λŠ” μ½”λ“œμ— λ‹¨μœ„ ν…ŒμŠ€νŠΈλ₯Ό μΆ”κ°€ν•˜λ©΄ μ–΄μ©” 수 없이 λ‚΄λΆ€ κ΅¬ν˜„μ„ 검증해야 ν•  λ•Œλ„ μžˆμŠ΅λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄ λ ˆκ±°μ‹œ μ½”λ“œμ— changeEmail() λ©”μ„œλ“œλŠ” 이메일을 λ³€κ²½ν•˜λŠ” κΈ°λŠ₯을 κ΅¬ν˜„ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

public void changeEmail(String id, String email) {
	int cnt = userDao.countById(id);
	if(cnt == 0) {
		throw new NoUserException();
	}
	userDao.updateEmail(id, email);
}

λ ˆκ±°μ‹œ μ½”λ“œμ—μ„œ DAOλŠ” λ‹€μ–‘ν•œ update, select λ“±μ˜ λ©”μ„œλ“œλ₯Ό μ •μ˜ν•˜κ³  μžˆλŠ” κ²½μš°κ°€ λ§ŽμŠ΅λ‹ˆλ‹€.
λ”°λΌμ„œ λ©”λͺ¨λ¦¬λ₯Ό μ΄μš©ν•œ κ°€μ§œ κ΅¬ν˜„μœΌλ‘œ λŒ€μ²΄ν•˜κΈ°κ°€ 쉽지 μ•ŠμŠ΅λ‹ˆλ‹€. 이런 경우 λͺ¨μ˜ 객체λ₯Ό 많이 ν™œμš©ν•©λ‹ˆλ‹€.

@Test
void changeEmailSuccessfully() {
	given(mockDao.countById(Mockito.anyString())).willReturn();

	emailService.changeEmail("id", "new@somehost.com");

	then(mockDao).should()
		.updateEmail(Mockito.anyString(), Mockito.matches("new@somehost.com");
}

이 μ½”λ“œλŠ” 이메일을 μˆ˜μ •ν–ˆλŠ”μ§€ ν™•μΈν•˜κΈ° μœ„ν•΄ updateEmail() λ©”μ„œλ“œ 호좜 μ—¬λΆ€λ₯Ό ν™•μΈν•©λ‹ˆλ‹€.

κΈ°λŠ₯이 μ •μƒμ μœΌλ‘œ λ™μž‘ν•˜λŠ”μ§€ 확인할 μˆ˜λ‹¨μ΄ κ΅¬ν˜„ 검증밖에 μ—†λ‹€λ©΄ λͺ¨μ˜ 객체λ₯Ό μ‚¬μš©ν•΄μ„œ ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό μž‘μ„±ν•΄μ•Ό ν•˜μ§€λ§Œ 일단 ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό μž‘μ„±ν•œ λ’€μ—λŠ” μ μ§„μ μœΌλ‘œ μ½”λ“œλ₯Ό λ¦¬νŒ©ν† λ§ν•΄μ„œ κ΅¬ν˜„μ΄ μ•„λ‹Œ κ²°κ³Όλ₯Ό 검증할 수 μžˆλ„λ‘ μ‹œλ„ν•΄μ•Ό ν•©λ‹ˆλ‹€.

셋업을 μ΄μš©ν•΄ μ€‘λ³΅λœ 상황을 μ„€μ •ν•˜μ§€ μ•ŠκΈ°

@BeforeEach
void setUp() {
	changeService = new ChangeUserService(memoryRepository);
	memoryRepository.save(
		new User("id", "name", "pw", new Address("μ„œμšΈ", "뢁뢀")
	);
}

@Test
void noUser() {
	assertThrows(
		UserNotFoundException.class,
		()-> changeService.ChangeAddress("id2", new Address("μ„œμšΈ", "남뢀"))
	);
}

@Test
void changeAddress() {
	changeService.changeAddress("id", new Address("μ„œμšΈ", "남뢀"));

	User user = memoryRepository.findById("id");
	assertEquals("μ„œμšΈ", user.getAddress().getCity());
}

@Test
void changePw() {
	changeService.changePw("id", "pw", "newpw");

	User user = memoryRepository.findById("id");
	assertTrue(user.matchPassword("newpw"));
}

@Test
void pwNotMatch() {
	assertThrows(
		IdPwNotMatchException.class,
		()->changeService.changePw("id", "pw2", "newpw")
	);
}

μ€‘λ³΅λœ μ½”λ“œλ₯Ό μ œκ±°ν•˜κΈ° μœ„ν•΄ @BeforeEach 같은 λ©”μ„œλ“œλ₯Ό μ΄μš©ν•  수 μžˆμ§€λ§Œ, μ΄λŠ” ν…ŒμŠ€νŠΈλ₯Ό κΉ¨μ§€κΈ° μ‰¬μš΄ ꡬ쑰둜 λ§Œλ“€ 수 μžˆμŠ΅λ‹ˆλ‹€. ν…ŒμŠ€νŠΈ λ©”μ„œλ“œλŠ” 독립적인 ν”„λ‘œκ·Έλž¨μœΌλ‘œμ„œ, 상황 ꡬ성 μ½”λ“œκ°€ λ©”μ„œλ“œ 내에 μžˆμ–΄μ•Ό 자체적으둜 μΆ©λΆ„νžˆ μ„€λͺ…될 수 μžˆμŠ΅λ‹ˆλ‹€.

@BeforeEach
void setUp() {
	changeService = new ChangeUserService(memoryRepository);
	memoryRepository.save(
		new User("id", "name", "pw2", new Address("μ„œμšΈ", "뢁뢀")
	);
}

톡합 ν…ŒμŠ€νŠΈμ—μ„œ 데이터 곡유 μ£Όμ˜ν•˜κΈ°

@SpringBootTest
@Sql("classpath:init-data.sql")
public class UserRegisterIntTestUsingSql {
	@Autowired
	private UserRegister register;
	@Autowired
	private JdbcTemplate jdbcTemplate;

	@Test
	void 동일IDκ°€_이미_μ‘΄μž¬ν•˜λ©΄_μ΅μ…‰μ…˜() {
		// μ‹€ν–‰ κ²°κ³Ό 확인
		assertThrows(DupIdException.class,
			()->register.register("cbk", "strongpw", "email@email.com")
		);
	}

	@Test
	void μ‘΄μž¬ν•˜μ§€_μ•ŠμœΌλ©΄_μ €μž₯함() {
		// μ‹€ν–‰
		register.register("cbk2", "strongpw", "email@email.com");
		// ...
	}
}

셋업을 μ΄μš©ν•œ 상황 μ„€μ •κ³Ό λΉ„μŠ·ν•œ κ²ƒμœΌλ‘œ 톡합 ν…ŒμŠ€νŠΈμ˜ DB 데이터 μ΄ˆκΈ°ν™”κ°€ μžˆμŠ΅λ‹ˆλ‹€.
DB 연동을 ν¬ν•¨ν•œ 톡합 ν…ŒμŠ€νŠΈλ₯Ό μ‹€ν–‰ν•˜λ €λ©΄ DB 데이터λ₯Ό μ•Œλ§žκ²Œ ꡬ성해야 ν•©λ‹ˆλ‹€.
μŠ€ν”„λ§ ν”„λ ˆμž„μ›Œν¬μ—μ„œλŠ” @Sql μ• λ…Έν…Œμ΄μ…˜μ„ μ‚¬μš©ν•˜μ—¬ ν…ŒμŠ€νŠΈλ₯Ό μ‹€ν–‰ν•˜κΈ° μ „ νŠΉμ • 쿼리λ₯Ό μ‹€ν–‰ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

@Sql μ• λ…Έν…Œμ΄μ…˜μœΌλ‘œ μ§€μ •ν•œ sql νŒŒμΌμ€ λ‹€μŒκ³Ό 같이 ν…ŒμŠ€νŠΈμ— ν•„μš”ν•œ 데이터λ₯Ό μ΄ˆκΈ°ν™”ν•©λ‹ˆλ‹€.

truncate table user;
insert into user values ('cbk', 'pw', 'cbk@cbk.com');
insert into user values ('tddhit', 'pw1', 'tddhit@ilovetdd.com')

이 μΏΌλ¦¬λŠ” μ—¬λŸ¬ ν…ŒμŠ€νŠΈκ°€ λ™μΌν•œ 데이터λ₯Ό μ‚¬μš©ν•  수 있게 λ§Œλ“€μ–΄μ£Όμ–΄μ„œ 톡합 ν…ŒμŠ€νŠΈ λ©”μ„œλ“œλŠ” 데이터 μ΄ˆκΈ°ν™”λ₯Ό μœ„ν•œ μ½”λ“œλ₯Ό μž‘μ„±ν•˜μ§€ μ•Šμ•„λ„ λ©λ‹ˆλ‹€. 이 방식은 νŽΈλ¦¬ν•˜μ§€λ§Œ μ…‹μ—… λ©”μ„œλ“œμ™€ λ§ˆμ°¬κ°€μ§€λ‘œ 쿼리 νŒŒμΌμ„ 쑰금만 변경해도 ν…ŒμŠ€νŠΈκ°€ 깨질 수 있고 κ²°κ΅­ μœ μ§€λ³΄μˆ˜λ₯Ό μ–΄λ ΅κ²Œ λ§Œλ“­λ‹ˆλ‹€.

톡합 ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό λ§Œλ“€ λ•ŒλŠ” λ‹€μŒ 두 κ°€μ§€λ‘œ μ΄ˆκΈ°ν™” 데이터λ₯Ό λ‚˜λˆ μ„œ 생각해야 ν•©λ‹ˆλ‹€.

  • λͺ¨λ“  ν…ŒμŠ€νŠΈκ°€ 같은 값을 μ‚¬μš©ν•˜λŠ” 데이터 : 예) μ½”λ“œκ°’ 데이터
  • ν…ŒμŠ€νŠΈ λ©”μ„œλ“œμ—μ„œλ§Œ ν•„μš”ν•œ 데이터 : 예) 쀑볡 ID 검사λ₯Ό μœ„ν•œ νšŒμ› 데이터

μ½”λ“œκ°’ λ°μ΄ν„°λŠ” 거의 λ°”λ€Œμ§€ μ•ŠκΈ° λ•Œλ¬Έμ— λͺ¨λ“  ν…ŒμŠ€νŠΈκ°€ λ™μΌν•œ μ½”λ“œκ°’ 데이터λ₯Ό μ‚¬μš©ν•΄λ„ λ¬Έμ œκ°€ μ—†μŠ΅λ‹ˆλ‹€.

λ°˜λ©΄μ— νŠΉμ • ν…ŒμŠ€νŠΈ λ©”μ„œλ“œμ—λ§Œ 의미 μžˆλŠ” λ°μ΄ν„°λŠ” λͺ¨λ“  ν…ŒμŠ€νŠΈκ°€ κ³΅μœ ν•  ν•„μš”κ°€ μ—†μŠ΅λ‹ˆλ‹€.
이런 λ°μ΄ν„°λŠ” νŠΉμ • ν…ŒμŠ€νŠΈμ—μ„œλ§Œ μƒμ„±ν•΄μ„œ ν…ŒμŠ€νŠΈ μ½”λ“œκ°€ μ™„μ „νžˆ ν•˜λ‚˜κ°€ λ˜λ„λ‘ ν•΄μ•Ό ν•©λ‹ˆλ‹€.

@Test
void dupId() {
	// 상황
	jdbcTemplate.update(
		"insert into user values(?,?,?) "+
		"on duplicate key update password = ?, email = ?",
		"cbk", "pw", "cbk@cbk.com", "pw", "cbk@cbk.com");

	// μ‹€ν–‰ κ²°κ³Ό 확인
	assertThrows(DupIdException.class,
		()->register.register("cbk", "strongpw", "email@email.com")
	);
}

톡합 ν…ŒμŠ€νŠΈμ—μ„œ μ΄ˆκΈ°ν™” 데이터λ₯Ό λ‚˜λˆŒ λ•Œ, λͺ¨λ“  ν…ŒμŠ€νŠΈκ°€ 같은 값을 μ‚¬μš©ν•˜λŠ” 데이터와 ν…ŒμŠ€νŠΈ λ©”μ„œλ“œμ—μ„œλ§Œ ν•„μš”ν•œ 데이터λ₯Ό ꡬ뢄해야 ν•©λ‹ˆλ‹€.

톡합 ν…ŒμŠ€νŠΈμ˜ 상황 섀정을 μœ„ν•œ 보쑰 클래슀 μ‚¬μš©

톡합 ν…ŒμŠ€νŠΈμ—μ„œ 데이터 곡유 μ£Όμ˜ν•˜κΈ° 처럼 쀑볡 IDλ₯Ό κ°€μ§„ νšŒμ›μ΄ μ‘΄μž¬ν•˜λŠ” 상황을 λ§Œλ“€κΈ° μœ„ν•΄ ν•„μš”ν•œ νšŒμ› 데이터λ₯Ό μƒμ„±ν–ˆμŠ΅λ‹ˆλ‹€. λΉ„μŠ·ν•˜κ²Œ 이메일 μˆ˜μ • κΈ°λŠ₯을 ν…ŒμŠ€νŠΈν•  λ•Œμ—λ„ μœ μ‚¬ν•œ 쿼리λ₯΄ μ‹€ν–‰ν•΄μ•Ό ν•©λ‹ˆλ‹€. 각 ν…ŒμŠ€νŠΈ λ©”μ„œλ“œμ—μ„œ 상황을 직접 κ΅¬μ„±ν•¨μœΌλ‘œμ¨ ν…ŒμŠ€νŠΈ λ©”μ„œλ“œλ₯Ό λΆ„μ„ν•˜κΈ°λŠ” μ’‹μ§€λ§Œ λ°˜λŒ€λ‘œ 상황을 λ§Œλ“€κΈ° μœ„ν•œ μ½”λ“œκ°€ μ—¬λŸ¬ ν…ŒμŠ€νŠΈ μ½”λ“œμ— μ€‘λ³΅λ©λ‹ˆλ‹€.
ν…Œμ΄λΈ” μ΄λ¦„μ΄λ‚˜ 칼럼 이름이 λ°”λ€Œλ©΄ μ—¬λŸ¬ ν…ŒμŠ€νŠΈ λ©”μ„œλ“œλ₯Ό μˆ˜μ •ν•΄μ•Ό ν•˜λ―€λ‘œ μœ μ§€λ³΄μˆ˜μ— μ’‹μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

ν…ŒμŠ€νŠΈ λ©”μ„œλ“œμ—μ„œ 직접 상황을 κ΅¬μ„±ν•˜λ©΄μ„œ μ½”λ“œ 쀑볡을 μ—†μ• λŠ” 방법이 μžˆλŠ”λ°, 상황 섀정을 μœ„ν•œ 보쑰 클래슀λ₯Ό μ‚¬μš©ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€.

public class UserGivenHelper {
	private JdbcTemplate jdbcTemplate;

	public UserGivenHelper(JdbcTemplate jdbcTemplate) {
		this.jdbcTemplate = jdbcTemplate;
	}

	public void givenUser(String id, String pw, String email) {
		jdbcTemplate.update(
			"insert into user values (?,?,?) "+
			"on duplicate key update password = ?, email = ?",
			id, pw, email, pw, email);
	}
}

상황을 κ΅¬μ„±ν•˜κΈ° μœ„ν•œ 보쑰 클래슀λ₯Ό μ‚¬μš©ν•˜κ²Œλ˜λ©΄ μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€.

Autowired JdbcTemplate jdbcTemplate;
private UserGivenHelper given;

@BeforeEach
void setUp() {
	given = new UserGivenHelper(jdbcTemplate);
}

@Test
void dupId() {
	given.givenUser("cbk", "pw", "cbk@cbk.com");

	// μ‹€ν–‰ κ²°κ³Ό 확인
	assertThrows(DupIdException.class,
		()->register.register("cbk", "strongpw", "email@email.com")
	);
}

톡합 ν…ŒμŠ€νŠΈμ—μ„œ λ™μΌν•œ 상황 μ„€μ •μ΄λ‚˜ 검증을 λ°˜λ³΅ν•  λ•ŒλŠ” 보쑰 클래슀λ₯Ό μ‚¬μš©ν•˜μ—¬ μœ μ§€λ³΄μˆ˜λ₯Ό μš©μ΄ν•˜κ²Œ ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 이λ₯Ό 톡해 μ€‘λ³΅λœ μ½”λ“œλ₯Ό 쀄이고 ν…ŒμŠ€νŠΈ μ½”λ“œμ˜ 가독성과 μœ μ§€λ³΄μˆ˜μ„±μ„ 높일 수 μžˆμŠ΅λ‹ˆλ‹€.

μ‹€ν–‰ ν™˜κ²½μ΄ λ‹€λ₯΄λ‹€κ³  μ‹€νŒ¨ν•˜μ§€ μ•ŠκΈ°

같은 ν…ŒμŠ€νŠΈ λ©”μ„œλ“œκ°€ μ‹€ν–‰ ν™˜κ²½μ— 따라 μ„±κ³΅ν•˜κ±°λ‚˜ μ‹€νŒ¨ν•˜λ©΄ μ•ˆ λ©λ‹ˆλ‹€.
둜컬 κ°œλ°œν™˜κ²½, λΉŒλ“œ μ„œλ²„, λ§₯ OS λ“± μ—¬λŸ¬ ν™˜κ²½μ—μ„œ λ™μΌν•˜κ²Œ λ™μž‘ν•΄μ•Ό ν•©λ‹ˆλ‹€.
μ‹€ν–‰ ν™˜κ²½μ— 따라 λ¬Έμ œκ°€ λ˜λŠ” 예둜 파일 κ²½λ‘œκ°€ μžˆμŠ΅λ‹ˆλ‹€.

public class BulkLoaderTest {
	private String bulkFilePath = "d:\\mywork\\temp\\bulk.txt";

	@Test
	void load() {
		BulkLoader loader = new BulkLoader();
		loader.load(bulkFilePath);
		// ...
	}
}

이럴 경우 μƒλŒ€ 경둜λ₯Ό μ‚¬μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€.

public class BulkLoaderTest {
	private String bulkFilePath = "src/test/resources/bulk.txt";

	@Test
	void load() {
		BulkLoader loader = new BulkLoader();
		loader.load(bulkFilePath);
		// ...
	}
}

ν…ŒμŠ€νŠΈ μ½”λ“œμ—μ„œ νŒŒμΌμ„ μƒμ„±ν•˜λŠ” κ²½μš°μ—λ„ νŠΉμ • OSλ‚˜ 개발 ν™˜κ²½μ—λ”°λΌ μ˜¬λ°”λ₯΄κ²Œ λ™μž‘ν•˜λ„λ‘ μ£Όμ˜ν•΄μ•Ό ν•©λ‹ˆλ‹€.

public class ExporterTest {
	@Test
	void export() {
		String folder = System.getProperty("java.io.tempdir");
		Exporter exporter = new Exporter(folder);
		exporter.export("file.txt");

		Path path = Paths.get(folder, "file.txt");
		assertTrue(Files.exists(path));
	}
}

이 μ½”λ“œλŠ” μ‹€ν–‰ ν™˜κ²½μ— μ•Œλ§žμ€ μž„μ‹œ 폴더 경둜λ₯Ό κ΅¬ν•΄μ„œ λ™μž‘ν•©λ‹ˆλ‹€.

κ°„ν˜Ή νŠΉμ • OS ν™˜κ²½μ—μ„œλ§Œ μ‹€ν–‰ν•΄μ•Ό ν•˜λŠ” ν…ŒμŠ€νŠΈλ„ μžˆμŠ΅λ‹ˆλ‹€.
JUnit 5κ°€ μ œκ³΅ν•˜λŠ” @EnabledOnOs, @DisabledOnOs λ₯Ό μ΄μš©ν•˜λ©΄ λ©λ‹ˆλ‹€.

μ‹€ν–‰ μ‹œμ μ— 따라 μ‹€νŒ¨ν•˜μ§€ μ•ŠκΈ°

νšŒμ›μ˜ 만료 μ—¬λΆ€λ₯Ό ν™•μΈν•˜λŠ” ν…ŒμŠ€νŠΈκ²½μš° μ½”λ“œ μž‘μ„± μ‹œμ μ—λŠ” μ‚¬μš©ν•  수 μžˆμ§€λ§Œ 이후 μ‹œκ°„μ΄ μ§€λ‚˜λ©΄μ„œ μ‹€νŒ¨ν•˜λŠ” ν…ŒμŠ€νŠΈκ°€ λ©λ‹ˆλ‹€.

public class Member {
	private LocalDateTime expiryDate;

	public boolean isExpired() {
		return expiryDate.isBefore(LocalDateTime.now());
	}
}
@Test
void notExpired() {
	// ν…ŒμŠ€νŠΈ μ½”λ“œ μž‘μ„± μ‹œμ  2019.1.1
	LocalDateTime expiry = LocalDateTime.of(2019,12,31,0,0,0);
	Member m = Member.builder().expiryDate(expiry).build();
	assertFalse(m.isExpired());
}

이럴 경우 ν…ŒμŠ€νŠΈ μ½”λ“œμ—μ„œ λͺ…μ‹œμ μœΌλ‘œ μ‹œκ°„μ„ μ œμ–΄ν•  방법을 μ„ νƒν•˜λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€.

public class Member {
	private LocalDateTime expiryDate;

	public boolean passedExpiryDate(LocalDateTime time) {
		return expiryDate.isBefore(time);
	}
}
@Test
void notExpired() {
	LocalDateTime expiry = LocalDateTime.of(2019,12,31,0,0,0);
	Member m = Member.builder().expiryDate(expiry).build();
	assertFalse(m.passedExpiryDate(LocalDateTime.of(2019,12,30,0,0,0));
}

λ‹€λ₯Έ λ°©λ²•μœΌλ‘œλŠ” λ³„λ„μ˜ μ‹œκ°„ 클래슀λ₯Ό μž‘μ„±ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€.

public class BizClock {
	private static BizClock DEFAULT = new BizClock();
	private static BizClock instance = DEFAULT;

	public static void reset() {
		instance = DEFAULT;
	}

	public static LocalDateTime now() {
		return instance.timeNow();
	}

	protected void setInstance(BizClock bizClock) {
		BizClock.instance = bizClock;
	}
	
	public LocalDateTime timeNow() {
		return LocalDateTime.now();
	}
}
public class Member {
	private LocalDateTime expiryDate;

	public boolean isExpired() {
		return expiryDate.isBefore(BizClock.now());
	}

BizClock 클래슀의 setInstance() λ©”μ„œλ“œλ₯Ό μ‚¬μš©ν•˜λ©΄ instance 정적 ν•„λ“œλ₯Ό ꡐ체할 수 μžˆμœΌλ―€λ‘œ 클래슀λ₯Ό 상속받은 ν•˜μœ„ 클래슀λ₯Ό μ΄μš©ν•˜λ©΄ BizClock.now()μ—μ„œ ν˜„μž¬ μ‹œκ°„μ„ ꡬ할 수 μžˆμŠ΅λ‹ˆλ‹€.

class TestBizClock extends BizClock {
	private LocalDateTime now;

	public TestBizClock() {
		setInstance(this);
	}

	public void setNow(LocalDateTime now) {
		this.now = now;
	}
	
	@Override
	public LocalDateTime timeNow() {
		return now != null ? now : super.now();
	}
}
public class MemberTest {
	TestBizClock testClock = new TestBizClock();

	@AfterEach
	void resetClock() {
		BizClock.reset();
	}

	@Test
	void notExpired() {
		testClock.setNow(LocalDateTime.of(2019,1,1,13,0,0));
		LocalDateTime expiry = LocalDateTime.of(2019,12,31,0,0,0);
		Member m = Member.builder().expiryDate(expiry).build();
		assertFalse(m.isExpired());
	}

λžœλ€ν•˜κ²Œ μ‹€νŒ¨ν•˜μ§€ μ•ŠκΈ°

ν•„μš”ν•˜μ§€ μ•Šμ€ 값을 μ„€μ •ν•˜μ§€ μ•ŠκΈ°

λ‹¨μœ„ ν…ŒμŠ€νŠΈλ₯Ό μœ„ν•œ 객체 생성 보쑰 클래슀 μ‚¬μš©

μ‘°κ±΄λΆ€λ‘œ κ²€μ¦ν•˜μ§€ μ•ŠκΈ°

톡합 ν…ŒμŠ€νŠΈμ—μ„œ λΆˆν•„μš”ν•œ 연동 λ²”μœ„ ν™•λŒ€ν•˜μ§€ μ•ŠκΈ°

더 이상 μ“Έλͺ¨μ—†λŠ” ν…ŒμŠ€νŠΈ μ½”λ“œλŠ” μ œκ±°ν•˜κΈ°

ν…ŒμŠ€νŠΈ μ½”λ“œλŠ” λ‹¨μˆœν•œ 검증 도ꡬ가 μ•„λ‹ˆλΌ, μ†Œν”„νŠΈμ›¨μ–΄μ˜ ν’ˆμ§ˆμ„ μœ μ§€ν•˜κ³  κ°œμ„ ν•˜λŠ” μ€‘μš”ν•œ μžμ‚°μž…λ‹ˆλ‹€. λ”°λΌμ„œ ν…ŒμŠ€νŠΈ μ½”λ“œμ˜ μœ μ§€λ³΄μˆ˜μ„±μ„ λ†’μ΄λŠ” 것은 ν•„μˆ˜μ μ΄λ©°, 이λ₯Ό μœ„ν•΄ μœ„μ—μ„œ μ–ΈκΈ‰ν•œ 방법듀을 μ‹€μ²œν•˜λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€.

profile
μ–΄μ œμ˜ λ‚˜λ³΄λ‹€ μ„±μž₯ν•œ μ‚¬λžŒμ΄ 될 수 μžˆλ„λ‘ λ…Έλ ₯ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

0개의 λŒ“κΈ€