[TDD Chapter7] λμ
π λμμ νμμ±
[μΈλΆ μμΈμ΄ ν μ€νΈμ κ΄μ¬νλ μ£Όμ μ]
- ν μ€νΈ λμμμ νμΌ μμ€ν μ¬μ©
- ν μ€νΈ λμμμ DBλ‘λΆν° λ°μ΄ν°λ₯Ό μ‘°ννκ±°λ λ°μ΄ν°λ₯Ό μΆκ°
- ν μ€νΈ λμμμ μΈλΆμ HTTP μλ²μ ν΅μ
ν μ€νΈ λμμ΄ μμ κ°μ μΈλΆ μμΈμ μμ‘΄νμ¬ μ¬μ©νλ μΈλΆ API μλ²κ° μΌμμ μΌλ‘ μ₯μ κ° λ°μνλ©΄ ν μ€νΈλ₯Ό μννκ² μνν μ μλ€. λν, λ΄λΆ DBλ₯Ό μ¬μ©νλλΌλ λ°μ΄ν° ꡬμ±μ νμ κ°κ² μ§ννκΈ°λ μ΄λ ΅λ€.
λ€μκ³Ό κ°μ μΈλΆ μμΈμ ν μ€νΈ μμ±μ μ΄λ ΅κ² λ§λ€ λΏλ§ μλλΌ ν μ€νΈ κ²°κ³Όλ μμΈ‘ν μ μκ² λ§λ λ€.
ν μ€νΈ λμμμ μμ‘΄νλ μμΈ λλ¬Έμ ν μ€νΈκ° μ΄λ €μΈ λμλ λμμ μ¨μ ν μ€νΈλ₯Ό μ§ννλ€. μΈλΆ μμΈμΌλ‘ μΈν΄ ν μ€νΈκ° μ΄λ €μΈ λ, μΈλΆ μμΈμ λμ νλ λμμ΄ μΈλΆ μμΈμ λμ νμ¬ ν μ€νΈμ μ°Έμ¬νλ€.
package testdriven.chap07;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static testdriven.chap07.CardValidity.THEFT;
import static testdriven.chap07.CardValidity.VALID;
public class AutoDebitRegisterTest {
private AutoDebitRegister register;
@BeforeEach
void setUp(){
CardNumberValidator validator = new CardNumberValidator();
AutoDebitInfoRepository repository = new JPAAutoDebitInfoRepository();
register = new AutoDebitRegister(validator, repository);
}
@Test
void validCard(){
// μΈλΆ μ
체μμ λ°μ ν
μ€νΈμ© μ ν¨ν μΉ΄λλ²νΈ μ¬μ©
// λμμ μ¬μ©νμ¬ ν
μ€νΈ μ§ν
AutoDebitReq req = new AutoDebitReq("user1", "123412341234");
RegisterResult result = this.register.register(req);
assertEquals(VALID, result.getValidity());
}
@Test
void theftCard(){
// μΈλΆ μ
체μμ λ°μ λλ ν
μ€νΈμ© μΉ΄λ λ²νΈ μ¬μ©
// λμμ μ¬μ©νμ¬ ν
μ€νΈ μ§ν
AutoDebitReq req = new AutoDebitReq("user1", "1234567890123456");
RegisterResult result = this.register.register(req);
assertEquals(THEFT, result.getValidity());
}
}
π λμμ μ¬μ©ν μΈλΆ μν© νλ΄μ κ²°κ³Ό κ²μ¦
μλ₯Ό λ€μ΄,
μΈλΆμν© νλ΄μ κ²½μ°μλ μΉ΄λ μ 보 APIλ₯Ό λμ νμ¬ μ ν¨ν μΉ΄λ μ 보μΈμ§, λλ μΉ΄λ μ 보μΈμ§μ κ°μ μν©μ νλ΄λ΄κ±°λ νΉμ μ¬μ©μμ λν μλ μ΄μ²΄ μ λ³΄κ° μ΄λ―Έ λ±λ‘λμ΄ μκ±°λ λ±λ‘λμ΄ μμ§ μμ μν©μ νλ΄λΈλ€.
package testdriven.chap07;
import java.util.HashMap;
import java.util.Map;
/**
* μΈλΆ μν© νλ΄: νΉμ μ¬μ©μμ λν μλ μ΄μ²΄ μ λ³΄κ° μ΄λ―Έ λ±λ‘λμ΄ μκ±°λ, μμ§ μμ μν©μ νλ΄λ.
*/
// AutoDebitInfoRepository μ λμ
public class MemoryAutoDebitRepository implements AutoDebitInfoRepository{
// μμμ±μ μ 곡νμ§λ μμ§λ§, ν
μ€νΈμμ μ¬μ©ν μ μμ λ§νΌμ κΈ°λ₯μ μ 곡
private Map<String, AutoDebitInfo> infos = new HashMap<>();
@Override
public void save(AutoDebitInfo info) {
infos.put(info.getUserIdx(), info);
}
@Override
public AutoDebitInfo findOne(String userId) {
return infos.get(userId);
}
}
package testdriven.chap07;
/**
* μΈλΆ μν© νλ΄: μΉ΄λ μ 보 API λ₯Ό λμ νμ¬ μ ν¨ν μΉ΄λ λ²νΈ, λλ μΉ΄λ λ²νΈμ κ°μ μν©μ νλ΄λ.
*/
public class StubCardNumberValidator extends CardNumberValidator{
private String invalidNo;
private String theftNo;
// setInvalidNoλ‘ μ§μ λ μΉ΄λ λ²νΈμ λν΄μλ invalid, λλ¨Έμ§λ valid
public void setInvalidNo(String invalidNo){
this.invalidNo = invalidNo;
}
public void setTheftNo(String theftNo){
this.theftNo = theftNo;
}
@Override
public CardValidity validate(String cardNumber) {
// μ ν¨νμ§ μμ μΉ΄λ λ²νΈμΈ κ²½μ°
if(invalidNo != null && invalidNo.equals(cardNumber)){
return CardValidity.INVALID;
}
// λλ μΉ΄λ λ²νΈμΈ κ²½μ°
if(theftNo != null && theftNo.equals(cardNumber)){
return CardValidity.THEFT;
}
return CardValidity.VALID;
}
}
μ΄ λΏλ§ μλλΌ, κ²°κ³Ό κ²μ¦μΌλ‘ λμμ μ¬μ©ν λ, λ©λͺ¨λ¦¬λ₯Ό μ΄μ©ν΄ μ μ₯ κ²°κ³Όλ₯Ό νμΈν μ μλ€.
π λμμ μ’ λ₯
λμ μ’ λ₯ | μ€λͺ |
μ€ν (Stub) | ꡬνμ λ¨μν κ²μΌλ‘ λ체, ν μ€νΈμ λ§κ² λ¨μν μνλ λμμ μν |
κ°μ§(Fake) | μ νμλ μ ν©νμ§ μμ§λ§, μ€μ λμνλ ꡬν μ 곡 μ] DB λμ Map μ¬μ©νμ¬ λ°μ΄ν° μ μ₯νλ ν μ€νΈ μ§ν |
μ€νμ΄(Spy) | νΈμΆλ λ΄μ κΈ°λ‘, κΈ°λ‘ν λ΄μ©μ ν μ€νΈ κ²°κ³Όλ₯Ό κ²μ¦ν λ μ¬μ©νλ μ€ν |
λͺ¨μ (Mock) | κΈ°λν λλ‘ μνΈμμ©νλμ§ νμ κ²μ¦, κΈ°λνλλ‘ λμνμ§ μμΌλ©΄ μ΅μ μ ₯ λ°μ |
π νμ κ°μ κΈ°λ₯ ꡬν (+ λμ μ¬μ©)
ꡬννκΈ° μ λͺ¨λ κΈ°λ₯μ μ€κ²νλ κ²μ λΆκ°λ₯νλ€. νμ§λ§ λ¨μ κΈ°λ₯μ ꡬννκΈ°μ μμ μ΄λ€ κ΅¬μ± μμκ° νμνμ§ κ³ λ―Όνλ κ²μ μμ‘΄ λμμ λμΆν λ λμμ΄ λλ€.
π€ μ½ν μνΈμμ νμΈ κΈ°λ₯μ μ€ν μ¬μ©
// ν
μ€νΈ μ½λ
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class UserRegisterTest {
private UserRegister userRegister;
private StubWeakPasswordChecker stubWeakPasswordChecker = new StubWeakPasswordChecker();
@BeforeEach
void setUp(){
userRegister = new UserRegister(stubWeakPasswordChecker);
}
@DisplayName("μ½ν μνΈλ©΄ μ€ν¨")
@Test
void weakPassword(){
stubWeakPasswordChecker.setWeak(true); // μνΈκ° μ½νλλ‘ μλ΅
assertThrows(WeakPasswordException.class, () ->
userRegister.register("id", "pw", "email"));
}
}
public class WeakPasswordException extends RuntimeException{
}
public interface WeakPasswordChecker {
boolean checkPasswordWeak(String pw);
}
public class StubWeakPasswordChecker implements WeakPasswordChecker{
private boolean weak;
public void setWeak(boolean weak){
this.weak = weak;
}
@Override
public boolean checkPasswordWeak(String pw) {
return weak;
}
}
public class UserRegister {
private WeakPasswordChecker passwordChecker;
public UserRegister(WeakPasswordChecker passwordChecker){
this.passwordChecker = passwordChecker;
}
public void register(String id, String pw, String email){
if(passwordChecker.checkPasswordWeak(pw)){
throw new WeakPasswordException();
}
}
}
π€ 리ν¬μ§ν 리λ₯Ό κ°μ§λ‘ ꡬν
λμμ κ²°κ³Ό κ²μ¦μΌλ‘ Mapμ μ΄μ©νμ¬ μ€μ DB μ κ·Ό λμ , μ½λ λ΄μμ μ 보λ₯Ό μ μ₯ν ν μ μ₯μ΄ λμ΄ μλ€λ©΄ μλ¬μ²λ¦¬λ₯Ό νλ ν μ€νΈ μ½λλ₯Ό μμ±ν κ²μ΄λ€.
// μ μ₯ν μ 보λ₯Ό λ΄κ³ μλ Class
public class User {
private String id;
private String password;
private String email;
public User(String id, String password, String email){
this.id = id;
this.password = password;
this.email = email;
}
public String getId() {
return id;
}
public String getEmail() {
return email;
}
}
// DB μ κ·Ό λͺ
λ Ήμ΄
public interface UserRepository {
void save(User user);
User findById(String id);
}
// DB μ κ·Ό μ½λ
public class MemoryUserRepository implements UserRepository{
private Map<String, User> users = new HashMap<>();
@Override
public void save(User user) {
users.put(user.getId(), user);
}
@Override
public User findById(String id) {
return users.get(id);
}
}
public class UserRegister {
private WeakPasswordChecker passwordChecker;
private UserRepository userRepository;
public UserRegister(WeakPasswordChecker passwordChecker, UserRepository userRepository){
this.passwordChecker = passwordChecker;
this.userRepository = userRepository;
}
public void register(String id, String pw, String email){
if(passwordChecker.checkPasswordWeak(pw)){
throw new WeakPasswordException();
}
// μΆκ°λ μ½λ
User user = userRepository.findById(id);
if(user == null){
throw new DupIdException();
}
userRepository.save(new User(id, pw, email));
}
}
// ν
μ€νΈ μ½λ
@DisplayName("μ΄λ―Έ κ°μ μμ΄λκ° μ‘΄μ¬νλ©΄ κ°μ
μ€ν¨")
void dupIdExists(){
// μ΄λ―Έ κ°μ μμ΄λκ° μλ μν©μΌλ‘ λ§λ€κΈ°
fakeRepository.save(new User("id", "pw1", "email"));
assertThrows(DupIdException.class,
() -> userRegister.register("id", "pw2", "email"));
}
@DisplayName("κ°μ IDκ° μμΌλ©΄ κ°μ
μ±κ³΅ν¨")
void noDupId_RegisterSuccess(){
userRegister.register("id", "pw", "email");
User savedUser = fakeRepository.findById("id");
assertEquals("id", savedUser.getId());
assertEquals("email", savedUser.getEmail());
}
π€ μ΄λ©μΌ λ°μ‘ μ¬λΆλ₯Ό νμΈνκΈ° μν΄ μ€νμ΄ μ¬μ©
public interface EmailNotifier {
void sendRegisterEmail(String email);
}
public class SpyEmailNotifier implements EmailNotifier {
private boolean called;
private String email;
public boolean isCalled() {
return called;
}
public String getEmail() {
return email;
}
@Override
public void sendRegisterEmail(String email) {
this.called = true; // test μ½λμ assert True λ₯Ό ν΅κ³Όμν€κΈ° μν¨
this.email = email;
}
}
public class UserRegisterTest {
private UserRegister userRegister;
private StubWeakPasswordChecker stubWeakPasswordChecker = new StubWeakPasswordChecker();
private MemoryUserRepository fakeRepository = new MemoryUserRepository();
private SpyEmailNotifier spyEmailNotifier = new SpyEmailNotifier(); // μΆκ°
@BeforeEach
void setUp(){
userRegister = new UserRegister(stubWeakPasswordChecker, fakeRepository, spyEmailNotifier);
}
// ~~~~ μλ΅ ~~~~
@DisplayName("κ°μ
νλ©΄ λ©μΌμ μ μ‘ν¨")
@Test
void whenRegisterThenSendMail(){
userRegister.register("id", "pw", "email");
assertTrue(spyEmailNotifier.isCalled());
assertEquals(
"email",
spyEmailNotifier.getEmail()
);
}
}
public void register(String id, String pw, String email){
if(passwordChecker.checkPasswordWeak(pw)){
throw new WeakPasswordException();
}
User user = userRepository.findById(id);
if(user != null){
throw new DupIdException();
}
userRepository.save(new User(id, pw, email));
emailNotifier.sendRegisterEmail(email); // μ½λ μΆκ°
}
π€ λͺ¨μ κ°μ²΄λ‘ μ€ν κ³Ό μ€νμ΄ λ체
λͺ¨μ κ°μ²΄λ₯Ό μ΄μ©νμ¬ μμ μμ±ν μ½λλ₯Ό λ€μ μμ±νκ³ μ νλ€.
λͺ¨μ κ°μ²΄λ Mockito λꡬλ₯Ό μ΄μ©νμ¬ λ€μ μμ±ν μμ μΈλ°, Mockitoμ λ΄μ©μ μλ λ§ν¬λ‘ μ 리λ₯Ό ν΄ λμλ€.
https://monynony0203.tistory.com/126
package testdriven.chap07.login;
public class UserRegister {
private WeakPasswordChecker passwordChecker;
private UserRepository userRepository;
private EmailNotifier emailNotifier;
public UserRegister(WeakPasswordChecker passwordChecker, UserRepository userRepository, EmailNotifier spyEmailNotifier){
this.passwordChecker = passwordChecker;
this.userRepository = userRepository;
this.emailNotifier = spyEmailNotifier;
}
public void register(String id, String pw, String email){
if(passwordChecker.checkPasswordWeak(pw)){
throw new WeakPasswordException();
}
User user = userRepository.findById(id);
if(user != null){
throw new DupIdException();
}
userRepository.save(new User(id, pw, email));
emailNotifier.sendRegisterEmail(email);
}
}
// spy: μ§μ§λ‘ νΈμΆλμλμ§ νμΈ
@DisplayName("κ°μ
νλ©΄ λ©μΌμ μ μ‘ν¨")
@Test
void whenRegisterThenSendMail(){
userRegister.register("id", "pw", "email");
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class); // String νμ
μ μΈμ 보κ΄
then(mockEmailNotifier)
.should().sendRegisterEmail(captor.capture());
String realEmail = captor.getValue();
assertEquals("email", realEmail);
}
π μν©κ³Ό κ²°κ³Ό νμΈμ μν νμ λμ(μμ‘΄) λμΆκ³Ό λμ μ¬μ©
ν μ€νΈλ νΉμ ν μν©μμ κΈ°λ₯μ μ€ννκ³ κ²°κ³Όλ₯Ό νμΈνλ€. νμ§λ§, μ€μ ꡬνμ μ΄μ©νλ©΄ μν©μ λ§λ€κΈ° μ΄λ €μΈ λκ° μλ€. μλ₯Ό λ€λ©΄ λ©μλ λ΄λΆμ μΈλΆ μΉ΄λ μ 보 APIλ₯Ό μ°λνμ¬ retuurn μ νκ² λλ©΄ μ ν¨νμ§ μμ μΉ΄λλ²νΈλ₯Ό μν μν©μ ꡬμ±νκΈ° μ΄λ ΅λ€.
μ΄λ κΈ° λλ¬Έμ, ꡬν/μ μ΄νκΈ° νλ ꡬνμν©μ΄ μ‘΄μ¬νλ©΄ λ€μκ³Ό κ°μ΄ μμ‘΄μ λμΆνκ³ μ΄λ₯Ό λμμΌλ‘ λμ ν μ μλ€.
- μ μ΄νκΈ° νλ μΈλΆ μν©μ λ³λ νμ μΌλ‘ λΆλ¦¬
- ν μ€νΈ μ½λλ λ³λλ‘ λΆλ¦¬ν νμ μ λμμ μμ±
- μμ±ν λμμ ν μ€νΈ λμμ μμ±μ λ±μ μ΄μ©νμ¬ μ λ¬
- λμμ μ΄μ©νμ¬ μν© κ΅¬μ±
π λμκ³Ό κ°λ° μλ
TDD κ³Όμ μμ λμμ μ¬μ©νμ§ μκ³ μ€μ ꡬνμ νλ€λ©΄ λ§μ λ¬Έμ μν©μ΄ λ°μν κ²μ΄λ€.
κ·Έλ κΈ° λλ¬Έμ λμμ μ¬μ©νλ©΄ μ€μ ꡬν μμ΄λ λ€μν μν©μ λν΄ ν μ€νΈλ₯΄ ν μ μλ€. μΈλΆμ μΉ΄λ μ 보 μ 곡 APIμ μ°λν μ μλ κ²½μ°μλ ν μ€νΈλ₯Ό μ§νν μ μλ κ²μ΄λ€.
λν, μ€μ ꡬνμ΄ μμ΄λ μ€ν κ²°κ³Όλ₯Ό λ°λ‘ νμΈν μ μλ€.
λμμ μμ‘΄νλ λμ (DB, μΈλΆ API λ±)μ ꡬννμ§ μμλ ν μ€νΈλ₯Ό μμ±ν μ μκ² λ§λ€μ΄μ£Όκ³ , λκΈ° μκ°μ μ€μ¬μ£Όμ΄ κ°λ°μλλ ν₯μμν¬ μ μλ€.
π λͺ¨μ κ°μ²΄λ₯Ό κ³Όνκ² μ¬μ©νμ§ μκΈ°
λͺ¨μ κ°μ²΄λ μ€ν κ³Ό μ€νμ΄λ₯Ό μ§μνλ―λ‘ λμμΌλ‘ λͺ¨μ κ°μ²΄λ₯Ό λ§μ΄ μ¬μ©νλ€. νμ§λ§, λͺ¨μ κ°μ²΄λ₯Ό κ³Όνκ² μ¬μ©νλ©΄ μ€νλ € ν μ€νΈ μ½λκ° λ³΅μ‘ν΄μ§λ κ²½μ°λ λ°μν μ μλ€.
λͺ¨μ κ°μ²΄λ₯Ό μ΄μ©νλ©΄ λμ ν΄λμ€λ₯Ό λ§λ€μ§ μμλ λλκΉ νΈν μ μμ§λ§, κ²°κ³Ό κ°μ νμΈνλ μλ¨μΌλ‘ λͺ¨μ κ°μ²΄λ₯Ό μ¬μ©νκ² λλ©΄ κ²°κ³Ό κ²μ¦ μ½λκ° κΈΈμ΄μ§κ³ 볡μ‘ν΄μ§λ€.
κΈ°λ³Έμ μΌλ‘ λͺ¨μ κ°μ²΄λ λ©μλ νΈμΆ μ¬λΆλ₯Ό κ²μ¦νλ μλ¨μ΄κΈ° λλ¬Έμ ν μ€νΈ λμκ³Ό λͺ¨μ κ°μ²΄ κ°μ μνΈμμ©μ΄ μ‘°κΈλ§ λ°λμ΄λ ν μ€νΈκ° κΉ¨μ§κΈ° μ½λ€.
νΉν DAOλ Repositoryμκ°μ΄ μ μ₯μμ λν λμμ λͺ¨μ κ°μ²΄λ₯Ό μ¬μ©νλ κ²λ³΄λ€ λ©λͺ¨λ¦¬λ₯Ό μ΄μ©ν΄ κ°μ§ ꡬνμ μ¬μ©νλ κ²μ΄ ν μ€νΈ μ½λ κ΄λ¦¬ν λ μ 리νλ€.