DevLog ๐ถ
[Java] Classic TDD vs Mockist TDD ๋ณธ๋ฌธ
์ผ๋ง ์ ์ ์ฐํ ์ฝ์์ ์ฒด์ค ํผ๋๋ฐฑ 2 ๊ฐ์๋ฅผ ๋ค์ผ๋ฉด์, ๋ํธ๋ก์ดํธ ํํ (classicist) vs ๋ฐ๋ ํํ (Mockist)์ ๋ํด์ ๋ฃ๊ฒ ๋์๋ค. ๋ ๋ค ์ฒ์ ๋ค์ด๋ณธ ์ฉ์ด์ฌ์ ๊ถ๊ธํ ๋ง์์ ์ด๊ฒ์ ๊ฒ ์ฐพ์๋ณด๋ฉฐ ์์ฑํด๋ณด๊ณ ์ ํ๋ค.
(์ ๋ฐ์ ์ธ ๋ด์ฉ์ ํ ์ฝํก ๋ฐํ ๋ด์ฉ์ ๋ง์ด ์ฐธ๊ณ ํ์์ต๋๋ค ๐โ๏ธ)
โ๏ธ ๋จ์ ํ ์คํธ๊ฐ ๋ฌด์์ผ๊น?
๋จ์ ํ ์คํธ๋ ๋ค์๊ณผ ๊ฐ์ ํน์ง์ ๊ฐ์ ธ์ผ ํ๋ค.
1. Low-level ํ ์คํธ๋ฅผ ๋ค๋ฃจ์ด์ผ ํ๋ค.
2. ์ผ๋ฐ์ ์ธ ํ ์คํธ ๋๊ตฌ๋ฅผ ์ฌ์ฉํด์ผ ํ๋ค.
3. ๋นจ๋ผ์ผ ํ๋ค.
์ฌ๊ธฐ์ ๋จ์๋ ๋ฌด์์ผ๊น?
๋ง์ฝ, ๊ธฐ๋ฅ ๋จ์๋ผ๊ณ ๊ตฌ์ฑ๋๋ค๋ฉด ๊ฐ์ฒด์งํฅ์ ๊ด์ ์์๋ ํด๋น ๊ธฐ๋ฅ์ ๊ตฌํํ๋ ํด๋์ค์ ์งํฉ์ด ๋จ์๊ฐ ๋ ์๋ ์๋ค.
์ฐ๋ฆฌ๋ ์ด๋ฌํ ๋จ์ ํ ์คํธ๋ฅผ ์งํํ๊ธฐ ์ํด์, ์ด๋ ํ ๋จ์๊ฐ ์ํธ ์์ฉํ ์ ์๋ ์ค์ ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ๊ฑฐ๋, test double์ ํ์ฉํ ์ ์๋ค. ๋จ์ ํ ์คํธ๋ ํฌ๊ฒ 2๊ฐ์ง์ ์ ํ์ผ๋ก ๋๋๋๋ฐ, ์ด๋ ์๋์ ๊ฐ๋ค.
1. Sociable Test
: ์ด๋ ์ด๋ ํ ๋จ์์ 'ํ์'๋ฅผ ํ ์คํธํ๋๋ฐ ์ค์ ์ ๋๋ค. ์ธํฐํ์ด์ค ๋ฑ์ ํ์ฉํ์ฌ ์ง์ ์ ์ผ๋ก ํ ์คํธ๋ฅผ ์งํํ ์ ์๋ค.
test double์ ์ฌ์ฉํ์ง ์๊ณ ์์ ๋ชจ๋์ ํตํด์ ํ์ ๋ชจ๋์ ์ง์ ์ ์ผ๋ก ํ ์คํธํ๋ค.
2. Solitary Test
: ์ด๋ ํ ๋จ์์ ํ์๊ฐ ๋ค๋ฅธ ํ์์ ์ข ์๋์ด ์๋ค๋ฉด, ์ต๋ํ ๋ ๋ฆฝ์ ์ผ๋ก ๊ฐ์ ธ๊ฐ์ ํ ์คํธ๋ฅผ ์งํํ๋ ๊ฒ์ด๋ค.
์ด๋, ๋ ๋ฆฝ์ ์ผ๋ก ๊ฐ์ ธ๊ฐ๊ธฐ ์ํด ๋ค๋ฅธ ํ์์ ๋ํด์ test double์ ์ฌ์ฉํ๊ณค ํ๋ค.
ํํ ์๋น์ค ๊ณ์ธต์ ๋ํด์ ํ ์คํธ๋ฅผ ํ ๋, ๋ค์ํ ์๋น์ค ๊ณ์ธต์ด ์ฎ์ฌ ์์ผ๋ฉด ๊ธฐ๋ฅ ๋จ์๋ก ํ ์คํธ๋ฅผ ํ๋๋ผ๋ ๋ค๋ฅธ ์๋น์ค ๊ณ์ธต์ ๊ธฐ๋ฅ๊น์ง ํจ๊ป ํ ์คํธ๋ฅผ ํ๋ฉด์ ์ผ์ข ์ ํตํฉ ํ ์คํธ์ฒ๋ผ ๋๊ปด์ง ์ ์๋ค.
sociable test๋ฅผ ์งํฅํ๋ ์ฌ๋๋ค์ classicist, solitary test๋ฅผ ์งํฅํ๋ ์ฌ๋๋ค์ mockist๋ผ๊ณ ๋ถ๋ฅธ๋ค.
โ๏ธ ์ฉ์ด ์ ๋ฆฌ
์์ํ๊ธฐ ์์, ํ ์คํธ ํจํด์์ ์ฌ์ฉํ๋ ์ฉ์ด๋ค์ ๋ํด ๊ฐ๋ตํ๊ฒ ์ ๋ฆฌํ๊ณ ๊ฐ์.
๐ฌ SUT
: System Under Test, ํ ์คํธ ๋์ ์์คํ ์ผ๋ก ์ผ์ข ์ ํ ์คํธ๋ฅผ ํ๋ ค๋ ๋์์ ์๋ฏธํ๋ค.
์ธ๋ถ์ ์ผ๋ก AUT (application Under Test), MUT (Method Under Test), CUT (Class Under Test), OUT (Object Under Test) ๋ฑ์ผ๋ก ๋๋๊ธฐ๋ ํ๋ค.
๐ฌ ํ๋ ฅ ๊ฐ์ฒด
SUT๊ฐ ์์กดํ๋ ํด๋์ค๋ก, SUT์ ์ฐ๊ด๊ด๊ณ๋ฅผ ๊ฐ์ง๊ณ ์๋ ํด๋์ค๋ฅผ ์๋ฏธํ๋ค.
โ๏ธ Test Double
์ค์ ๊ฐ์ฒด์ ํ์๊ฐ ํ ์คํธํ๊ธฐ ์ด๋ ต๊ฑฐ๋ ๋น์ฉ์ด ๋ง์ด ๋ค ๋ ์ฌ์ฉํ๋ ๊ฐ์ง ๊ฐ์ฒด์ด๋ค.
๋ํ์ ์ผ๋ก๋ Dummy, Fake, Mock, Spy, Stub ๋ฑ์ด ์กด์ฌํ๋ค.
๊ฐ๊ฐ์ ๋ํด ๊ฐ๋จํ๊ฒ ์ค๋ช ํด๋ณด์๋ฉด ๋ค์๊ณผ ๊ฐ๋ค. ๊ฐ๋จํ ์์๋ฅผ ๋ค๊ธฐ ์ํด์ ๋ค์๊ณผ ๊ฐ์ ์ฝ๋๊ฐ ์๋ค๊ณ ๊ฐ์ ํด๋ณด์.
import java.util.Optional;
public interface UserDao {
Optional<UserEntity> findByName(final String name);
Long save(final String name);
}
// UserEntity
public final class UserEntity {
private Long id;
private final String name;
public UserEntity(final Long id, final String name) {
this.id = id;
this.name = name;
}
}
์ด๋ฒ ์ฒด์ค ๋ฏธ์ ์์ ์ฌ์ฉํ๋ ์ฝ๋๋ก, ์ฌ์ฉ์์ ์ด๋ฆ์ ๋ฐํ์ผ๋ก ์กฐํํ๋ ๊ธฐ๋ฅ๊ณผ ์ ์ฅํ๋ ๊ธฐ๋ฅ์ด ๋ด๊ฒจ์๋ค.
์์ ์ฝ๋๋ ์ค์ ํ๋ก๋์ ์ฝ๋์์๋ ๋ค์๊ณผ ๊ฐ์ด ๊ตฌํ๋์ด ์๋ค.
public final class UserDaoImpl implements UserDao {
private final JdbcTemplate jdbcTemplate;
public UserDaoImpl(final JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public Optional<UserEntity> findByName(final String name) {
final String query = "SELECT * FROM user WHERE user_name = ?";
return jdbcTemplate.findOne(query, (resultSet -> new UserEntity(
resultSet.getLong("user_id"),
resultSet.getString("user_name"))
), name);
}
@Override
public Long save(final String name) {
final String query = "INSERT INTO user(user_name) VALUES(?)";
return jdbcTemplate.executeUpdate(query, name);
}
}
์ด ์ฝ๋๋ฅผ ๊ฐ๊ฐ์ ํ ์คํธ ๋๋ธ๋ก ์ ์ฉํด๋ณด์.
1. ์คํ (Stub)
- ํธ์ถ์ด ๋์์ ๋ ๋ฏธ๋ฆฌ ์ ํด์ง ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํ๋ ๊ฐ์ฒด์ด๋ค.
public class MockUserDao implements UserDao {
@Override
public Optional<UserEntity> findByName(final String name) {
return Optional.of(new UserEntity(1L, "journey"));
}
@Override
public Long save(final String name) {
return 1L;
}
}
์์ ๊ฐ์ด, ํด๋น ๋ฉ์๋๋ฅผ ๋ฐํํ๋ฉด ํญ์ ๊ฐ์ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํ๋๋ก ํ๋ ๊ฒ์ด๋ค.
2. ํ์ดํฌ (Fake)
- ์ค์ ๋ก ๋์ํ์ง๋ง, ์กฐ๊ธ ๋ ๊ฐ๋ฒผ์ด ๋ฒ์ ์ ๊ตฌํ์ ์ฌ์ฉํ์ฌ ํ ์คํธ์ ์๋๋ฅผ ๋์ด๋ ๋ฐฉ๋ฒ์ด๋ค.
public class MockUserDao implements UserDao {
private final Map<Long, UserEntity> STORAGE = new ConcurrentHashMap<>();
private final AtomicLong pk = new AtomicLong(0L);
@Override
public Optional<UserEntity> findByName(final String name) {
return STORAGE.entrySet().stream()
.filter(entry -> entry.getValue().getName().equals(name))
.map(entry -> new UserEntity(entry.getKey(), entry.getValue().getName()))
.findFirst();
}
@Override
public Long save(final String name) {
STORAGE.put(pk.addAndGet(1L), new UserEntity(name));
return pk.longValue();
}
}
์ค์ ๋ก ๋๋ ์ด๋ฒ ์ฒด์ค ๋ฏธ์ ์์ ์์ ๊ฐ์ ๋ฐฉ์์ผ๋ก ๊ตฌํํ์๋ค.
ConcurrentHashMap ๊ฐ์ฒด๋ฅผ ์ผ์ข ์ DB ์คํ ๋ฆฌ์ง๋ก ์ฌ์ฉํ์ฌ์ ์ค์ ๋ก ๊ฐ์ ์ฝ์ ํ๊ณ , ์กฐํํ๋ ๊ธฐ๋ฅ์ด ๋์ํ๋๋ก ๋ง๋ค์๋ค.
3. ์คํ์ด (Spy)
- Stub์ ์ญํ ์ ๊ฐ์ง๋ฉด์, ํธ์ถ๋ ํจ์์ ์ ๋ณด๋ฅผ ๊ธฐ๋กํ์ฌ ๋์ค์ ํ ์คํธ์์ ์ฌ์ฉํ ์ ์๋ ๊ฐ์ฒด๋ฅผ ์๋ฏธํ๋ค.
public class MockUserDao implements UserDao {
private final List<String> queries = new ArrayList<>();
@Override
public Optional<UserEntity> findByName(final String name) {
queries.add("findByName");
return Optional.of(new UserEntity(1L, "journey"));
}
@Override
public Long save(final String name) {
queries.add("save");
return 1L;
}
public boolean isCall(final String query) {
return queries.contains(query);
}
}
๋ด๊ฐ ์ดํดํ ๋ฐ๋ก๋ ์ด๋ฐ ์์ผ๋ก Stub์ ๊ธฐ๋ฅ์ ์ด๋ค ์์ผ๋ก ํธ์ถ๋์๋์ง์ ๋ํ ์ ๋ณด๋ ํฌํจํ๋ ๊ฒ์ด๋ผ๊ณ ์๊ฐํ๋ค.
๊ทธ๋์ ๋์ค์ ์ธ๋ถ์ ์ผ๋ก isCall์ ํตํด์ ํด๋น ๋ฉ์๋๊ฐ ํธ์ถ๋์๋์ง ํ์ธํ ์ ์๋๋ก ํ๋ ๊ฒ ์๋๊น ์ถ๋ค.
4. ๋๋ฏธ (Dummy)
- ๋จ์ง ์ธ์คํด์คํ๋ ๊ฐ์ฒด๊ฐ ํ์ํ๋ฉด์, ํด๋น ๊ฐ์ฒด์ ๊ธฐ๋ฅ์ ํ์์์ด ์ค์ ๋ก ์ฌ์ฉ๋์ง ์์ ๊ฒฝ์ฐ ์ฌ์ฉํ๋ค.
์์ ์์ ์ฝ๋๋ณด๋ค๋ ๋ค๋ฅธ ๊ฒฝ์ฐ๊ฐ ๋ ์ ํฉํ ๊ฒ ๊ฐ์์, ์์ ๋ฅผ ๋ฐ๊พธ์ด๋ณด์๋ค.
public class MockUserDao {
public void printUserInfo() {
// nothing.
}
}
์ด๋ฐ ์์ผ๋ก ๋จ์ํ printUserInfo()๋ฅผ ํธ์ถํ๋ค๋ ๊ฒ๋ง ํ์ํ ๋ ์ฌ์ฉํ๋ค.
๊ฐ์ธ์ ์ธ ์๊ฐ์ผ๋ก๋, ์ธ๋ถ api์ ๋ํด์ ํธ์ถ์ ํด์ผ ํ๋ ๊ฒฝ์ฐ (ex. ์๋ฆผ ์ ์ก ๊ฐ์ด ๋ฐํ๊ฐ์ ํ์ ์๋ ๊ฒฝ์ฐ)๋ ๋ณต์กํ ui ๋ก์ง์ด ํ์ํ๋ค๋ฉด ๋๋ฏธ๋ฅผ ํ์ฉํ ์ ์์ ๊ฒ์ผ๋ก ํ๋จ๋๋ค.
5. ๋ชฉ (Mock)
ํธ์ถ์ ๋ํด์ ์ด๋ค ๊ฒ์ ํธ์ถํ๊ณ ์ด๋ค ๊ฒ์ ๋ฐํํ ์ง ๋ฏธ๋ฆฌ ์ง์ ํ๋ ๊ฒ์ด๋ค.
๋ํ์ ์ผ๋ก Mockito ํ๋ ์์ํฌ๊ฐ ์กด์ฌํ๋ค. ์ด ์ญ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ค๋ฉด ๋ค์๊ณผ ๊ฐ์ด ๊ตฌํํ ์ ์๋ค.
UserDao userDao = mock(UserDao.class);
// ๋ฌด์์ ํธ์ถํ๋ฉด ์ด๋ค ์์ผ๋ก ๋ฐํํ ์ง ๋ฏธ๋ฆฌ ์ ์
when(userDao.findByName(any())).thenReturn(new UserEntity(1L, "journey"));
when(userDao.save(any())).thenReturn(1L);
ํํ ๋ค์๊ณผ ๊ฐ์ด ๋๋์ด์ง๋ค๊ณ ๋ ํ๋ค. (์ด๋ ค์ด ๋ด์ฉ์ด๋ ์ฐ์ ์ ๊ทธ๋ฅ ์ฝ๊ณ ๋์ด๊ฐ์!)
์ด๋ฆ | ๋ชฉ์ | ๋์์ด ์๋? | SUT์ ๊ฐ์ ์ ๋ ฅ ์ฃผ์ |
SUT ๊ฐ์ ์ถ๋ ฅ ์ฒ๋ฆฌ |
ํ ์คํธ๊ฐ ์ ๊ณตํ๋ ๊ฐ |
Stub | SUT์ ๊ฐ์ ์ ๋ ฅ ๊ฒ์ฆ |
์์ | ํจ | ๋ฌด์ | ์ ๋ ฅ๊ฐ |
Fake | ๋น ๋ฅธ ์คํ | ์์ | ์์ | ๊ฐ์ ์ถ๋ ฅ ์ฌ์ฉ | ์์ |
Spy | SUT์ ๊ฐ์ ์ถ๋ ฅ ๊ฒ์ฆ |
์์ | ์ ํ ์ฌํญ | ๋์ค์ ๊ฒ์ฆํ๋ ค๊ณ ํจ | ์ ๋ ฅ (์ ํ ์ฌํญ) |
Dummy | ์์ฑ์ด๋ ๋ฉ์๋ ์ธ์๋ก์ ์๋ | ์์ | ์ ํจ | ์ ํจ | ์์ |
Mock | SUT์ ๊ฐ์ ์ถ๋ ฅ ๊ฒ์ฆ |
์์ | ์ ํ ์ฌํญ | ๊ธฐ๋๊ฐ๊ณผ ๋น๊ตํ์ฌ ์ ํํ์ง ๊ฒ์ฆํจ |
์ ๋ ฅ, ๊ธฐ๋ ์ถ๋ ฅ |
โ๏ธ Classicist - ์ํ ๊ฒ์ฆ / Mockist - ํ์ ๊ฒ์ฆ
classicist์์๋ ์ฃผ๋ก ๊ฒ์ฆ์ ํ๊ธฐ ์ํด์ ์ด๋ ํ ์ํฉ์ ์ํํ ๋ค์, ํด๋น ๊ฐ์ฒด์ ๋ด๋ถ ์ํ๋ฅผ ํ์ธํ๊ฒ ๋๋ค.
์ด๋, ํ์๊ฐ ์ํ๋ ๋ค์ ์ด๋ ํ ์ํ์ธ์ง ํ์ธํ๊ธฐ ์ํด์ getter ๊ฐ์ ๋ฉ์๋ ๋ฑ์ด ์ถ๊ฐ๋ ์ ์๋ค.
์์์ ๋งํ๋ 'stub'์ ๊ฒฝ์ฐ ์ํ ๊ฒ์ฆ์ ์ผ๋ถ๋ผ๊ณ ๋ณผ ์ ์๋ค.
public class CrewServiceTest {
private CrewService crewService;
private CrewRepository crewRepository;
@Before
public void setUp() {
crewRepository = mock(CrewRepository.class);
crewService = new CrewService(crewRepository);
}
@Test
public void createCrew() {
// given
Crew crew = new Crew("journey");
// when
crewRepository.createCrew(crew);
// then
Crew createdCrew = crewRepository.findByName("journey");
assertThat(createdCrew.getName()).isEqualTo("journey");
}
}
createCrew()๋ฅผ ํตํด์ ํด๋น ํฌ๋ฃจ๊ฐ ์ ์์ฑ๋์๋์ง ํ ์คํธ ํ๋ ์ฝ๋์ด๋ค.
crew๋ฅผ ์ฝ์ ํ ๋ค์, ํฌ๋ฃจ์ ์ด๋ฆ์ ํตํด์ ํด๋น ํฌ๋ฃจ๋ฅผ ์ฐพ์ ๋ค์ getName()์ ํตํด์ ์ฐ๋ฆฌ๊ฐ ์์ฑํ ํฌ๋ฃจ์ธ์ง ํ์ธํ ์ ์๋ค.
์ด๋ฐ ์์ผ๋ก ํด๋น ํฌ๋ฃจ๋ฅผ ์ฝ์ ํ ๋ค, ๋ด๋ถ์ ๊ฐ์ฒด๊ฐ ์ ์์ฑ๋์๋์ง ์ํ๋ฅผ ํ์ธํ๋ ๊ฒ์ด๋ค.
๋ฐ๋ฉด์ Mockist์ ๊ฒฝ์ฐ ํน์ ํ ๋์์ ์ํํ๋์ง ํ์ธํ๋ ๊ฒ์ฆ ๋ฐฉ๋ฒ์ด๋ค.
์์์ ๋งํ๋ 'mock'์ ๊ฒฝ์ฐ ํ์ ๊ฒ์ฆ์ ์ผ๋ถ๋ผ๊ณ ๋ณผ ์ ์๋ค.
public class CrewServiceTest {
private CrewService crewService;
private CrewRepository crewRepository;
@Before
public void setUp() {
crewRepository = mock(CrewRepository.class);
crewService = new CrewService(crewRepository);
}
@Test
public void createCrew() {
// given
Crew crew = new Crew("journey");
given(crewRepository.save(crew)).willReturn(crew);
// when
crewRepository.createCrew(crew);
// then
verify(crewRepository).save(crew);
}
}
์์ ์ฝ๋์์ CrewService๋ CrewRepository๋ฅผ ์์กดํ๊ณ ์๋ ํํ์ด๋ค.
์ด๋, ํ ์คํธ ์ฝ๋์์ Crew ๊ฐ์ฒด๋ฅผ ์์ฑํ๊ณ , save ๋ฉ์๋๋ฅผ ํธ์ถํ๋ฉด ์์ฑํ crew๋ฅผ ๋ฐํํ๋๋ก ํด๋ณด์.
์ด๋, verify๋ฅผ ํตํด์ crew ๊ฐ์ฒด์ ํจ๊ป ํด๋น save ๋ฉ์๋๊ฐ ํธ์ถ๋์๋์ง ํ์ธํ๋ค.
save() ๋ฉ์๋์ ํธ์ถ์ ๋ํ ํ ์คํธ๋ฅผ ์งํํ๋ค๊ณ ๋ ๋ณผ ์ ์์ผ๋ฉฐ, ๋ง์ฝ ์์ ํ ์คํธ๊ฐ ์คํจํ๋ค๋ฉด save() ๋ฉ์๋๋ฅผ ํธ์ถํ์ง ์์๊ฑฐ๋, ์ธ์๋ก ๋๊ธด crew ๊ฐ์ฒด๊ฐ ์๋ชป๋์์์ ๋ปํ๊ฒ ๋๋ค.
์ด๋ฐ ์์ผ๋ก mockist๋ฅผ ์ฌ์ฉํ๊ฒ ๋๋ฉด getter ๊ฐ์ด ์ํ๋ฅผ ๋ฐ์์ฌ ๋ฉ์๋๋ ํ์์์ง๋ง, SUT์ ๋ํด ๊ตฌํ ๋ฐฉ์์ด ๋ ธ์ถ๋๋ค๋ ๋จ์ ์ด ์๋ค.
โ๏ธ ๊ฐ๊ฐ์ ์ฅ๋จ์ ์ ์ฒดํฌํด๋ณด๊ธฐ
๐ฌ ๋ฌธ์ ์ํฉ 1. ์ธ๋ถ API๋ฅผ ํธ์ถํ๋ ๊ฒฝ์ฐ
์์ ๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ ๋ํด์ ์๊ฐํด๋ณด์.
์ฌ์ฉ์๊ฐ ํฌ๋ฃจ๋ก์ ์ถ๊ฐ๋๊ธฐ ์ํด์๋ ์ฌ์ฉ์์ ์์ ๋ก๊ทธ์ธ ์ ๋ณด๊ฐ ํ์ํ๋ฉฐ, ์ธ๋ถ api๋ฅผ ํธ์ถํ๋ ์์ ์ด ํ์ํ๋ค.
๊ทธ๋ฌ๋, ์ด๋ฌํ ์ธ๋ถ api๋ ํญ์ ๋์ผํ ๊ฒฐ๊ณผ๊ฐ ๋์ฌ ๊ฒ์ด๋ผ๊ณ ๊ธฐ๋ํ ์๋ ์์ผ๋ฉฐ, ๋น์ฉ์ด ๋น์ธ๋ค.
(๋ง์ฝ ์ธ๋ถ api ์๋ฒ๊ฐ ๋ง๊ฐ์ก๋ค๋ฉด? ํน์, ๋ด๊ฐ ์์ฒญํ ์ ๋ณด๊ฐ ์ค๊ฐ์ ์ ์ค๋์๋ค๋ฉด?)
๊ทธ๋ ๊ธฐ ๋๋ฌธ์, ๋งค๋ฒ ์ธ๋ถ api๋ฅผ ํธ์ถํ์ฌ ํ ์คํธ ํ๋ ๊ฒ์ ๋ณ๋ก ์ข์ ๋ฐฉ๋ฒ์ ์๋๋ค.
์ด๋ฐ ๊ฒฝ์ฐ์์๋ mockist๊ฐ ๋ ์ข๋ค๊ณ ๋ณผ ์ ์๋ค. ์ธ๋ถ api๋ฅผ ํธ์ถํ๋ ๊ฒฝ์ฐ์๋ ๋จ์ํ ์์ธก์ ํด์ฃผ๋ฉด ๋๋ ๊ฒ์ด๋ค.
@Test
public void createCrew() {
Crew crew = new Crew("journey");
given(crewRepository.save(crew)).willReturn(crew);
given(oauthService.getUserInfo(crew.getEmail()).willReturn(...); // here!
crewRepository.createCrew(crew);
verify(crewRepository).save(crew);
verify(oauthService).getUserInfo(crew.getEmail());
}
classicist๋ ์ด๋ฐ ๊ฒฝ์ฐ์ ํญ์ ์ธ๋ถ api๋ฅผ ํธ์ถํ ๊น?
๊ทธ๋ ์ง๋ ์๋ค. ํ ์คํธ๋ฅผ ํ๊ธฐ ์ด๋ ต๋ค๋ฉด, test double์ ์ข ๋ฅ์ ์ข ์๋์ง ์๊ณ ๋ค๋ฅธ ๊ฒ์ ํ์ฉํ๋ฉด ๋๋ ๊ฒ์ด๋ค.
๐ฌ ๋ฌธ์ ์ํฉ 2. ์๊ตฌ์ฌํญ์ด ์ถ๊ฐ๋์์ ๊ฒฝ์ฐ
๋ค์๊ณผ ๊ฐ์ ์ํฉ์ ๋ค ์๋ ์๋ค.
๋ง์ฝ ํฌ๋ฃจ๋ฅผ ์ถ๊ฐํ๊ธฐ ์ ์, ๋ด๋ถ์ ์ผ๋ก MissionService๋ฅผ ํตํด์ ํด๋น ํฌ๋ฃจ์ ๋ฏธ์ ํํฉ์ ๋ํด ๊ฒ์ฆํ๋ค๊ณ ์๊ฐํด๋ณด์.
@Test
public void createCrew() {
// given
Crew crew = new Crew("journey");
// when
crewRepository.createCrew(crew);
// then
Crew createdCrew = crewRepository.findByName("journey");
assertThat(createdCrew.getName()).isEqualTo("journey");
}
classicist์ ๊ฒฝ์ฐ ์ฝ๋๊ฐ ๋ณ๊ฒฝ๋์ง ์๋๋ค. ์๋ํ๋ฉด, ๋ด๋ถ์ ์ผ๋ก createCrew()์์ ๋ฌด์จ ํ์๋ฅผ ํ๋ ํ ์คํธ๋ ์์ง ๋ชปํ๊ธฐ ๋๋ฌธ์ด๋ค. ๊ทธ์ ๋จ์ํ ์งํ์ ํ์์ ๋ ํฌ๋ฃจ๊ฐ ์ ์ฅ์ด ๋๊ณ , ํฌ๋ฃจ์ ๋ํ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๊ธฐ๋ง ํ๋ฉด ๋๋ค.
ํ๋ก๋์ ์ ์ฝ๋ ๋ณํ๊ฐ ํ ์คํธ ์ฝ๋์ ๋ณํ๋ฅผ ๋ง์ด ๋ผ์น์ง ์๊ธฐ ๋๋ฌธ์, ๊ธฐ๋ฅ ์ถ๊ฐ๊ฐ ์ํํด์ง๋ค.
@Test
public void createCrew() {
Crew crew = new Crew("journey");
given(crewRepository.save(crew)).willReturn(crew);
given(missionService.isAllMissionPass(crew.getEmail()).willReturn(...); // here!
crewRepository.createCrew(crew);
verify(crewRepository).save(crew);
verify(missionService).isAllMissionPass(crew.getEmail()); // here!
}
๋ฐ๋ฉด, mockist์ ๊ฒฝ์ฐ ์ฝ๋์ ๋ณํ๊ฐ ์๊ธธ ์๋ฐ์ ์๋ค.
์๋ํ๋ฉด, save์์ ๋ด๋ถ์ ์ผ๋ก ํธ์ถ๋๋ ํ์์ ๋ํด์ ๋ช ์์ ์ผ๋ก ํ ์คํธ๋ฅผ ์งํํด์ฃผ์ด์ผ ํ๊ธฐ ๋๋ฌธ์ด๋ค.
์์ ๊ฐ์ด ํด๋น ๋ฉ์๋๊ฐ ํธ์ถ๋์์ ๋ ์ด๋ค ํ์๋ฅผ ํ ์ง, ๊ทธ๋ฆฌ๊ณ ํธ์ถ๋์๋์ง์ ๋ํ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํด์ผ ํ๋ค. ๋ง์ฝ TDD๋ฅผ ์ ์ฉํ๋ค๋ฉด ์ด๋ฐ ๋ณํ์ ๊ณ์ ๋์ํด์ผ ํ๊ธฐ ๋๋ฌธ์ ์๋นํ ๊ท์ฐฎ์์ง ์๋ ์๋ค.
๐ฌ ๋ฌธ์ ์ํฉ 3. ๋ฒ๊ทธ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ
๋ง์ฝ, ์์ ์ํฉ์์ ๊ฐ์๊ธฐ MissionService์ ์ฅ์ ๊ฐ ๋ฐ์ํ๋ค๊ณ ์๊ฐํด๋ณด์.
์ด๋ ๊ฒ ๋๋ฉด classicist๋ก ๊ตฌํํ ์ฝ๋๋ ๋ด๋ถ์ ์ผ๋ก ์ฅ์ ๊ฐ ๋ฐ์ํ๋ฉด์ ํด๋น ํ ์คํธ๊น์ง ์คํจํ๊ฒ ๋ ๊ฒ์ด๋ค.
์ฆ, ํ ์คํธ๊ฐ ๊ฒฉ๋ฆฌ๋์ง ์๊ณ ์ธ๋ถ์์ ๋ด๋ถ ์์๋ค๊น์ง ํจ๊ป ํ ์คํธ ํ๋ฉด์ ์ฅ์ ๊ฐ ์ ํ๋๋ ๊ฒ์ด๋ค.
๋ง์ฝ MissionService๊ฐ CrewService๋ฟ๋ง ์๋๋ผ PrologService, JjimggongService์๋ ์ฐ๊ด์ด ์๋ค๊ณ ๊ฐ์ ํด๋ณด์.
์ด๋ ๊ฒ ๋๋ฉด ๋ค์๊ณผ ๊ฐ์ด ์ค๋ฅ๊ฐ ๋ชจ๋์ ์ ํ๋๋ค.
ํ์ง๋ง, mockist๋ ๋จ์ํ ๋ฒ๊ทธ๊ฐ ๋ฐ์ํ ํด๋์ค์ ๋ํด์๋ง ์คํจํ๊ฒ ๋๋ค.
ํ ์คํธ ์ฝ๋ ์์ผ๋ก ๋ด๋ถ ๋ก์ง์ ๋ชจ๋ ์ค์ ํธ์ถ์ด ์๋, ์ด๋ค ์์ผ๋ก ํธ์ถ๋ ๊ฒ์ธ์ง ์ ์ด๋ฅผ ํ๊ธฐ ๋๋ฌธ์ ์ํฅ์ด ์๋ ๊ฒ์ด๋ค.
ํ์ง๋ง, classicist๋ ์ด๋ฐ ์ค๋ฅ ์ ํ์ ํฌ๊ฒ ์ ๊ฒฝ์ฐ์ง ์๋๋ค.
์ด์ฐจํผ TDD๋ฅผ ์งํํ๊ฒ ๋๋ค๋ฉด ์์ ๋จ์ ํ ์คํธ๋ถํฐ ์์ํ์ฌ ํตํฉ ํ ์คํธ๊น์ง, ๊ณ์ํด์ ์ ์ฒด ํ ์คํธ ์ฝ๋๋ฅผ ๋๋ฆฌ๊ฒ ๋ ๊ฒ์ด๊ณ , ๊ทธ๋ฌ๋ฉด ํ ์คํธ๋ฅผ ๋๋ฆด ๋ ๋ง์ง๋ง์ผ๋ก ์์ ํ ํ์ผ์ด ์ค๋ฅ๊ฐ ๋ฐ์๋ ์ง์ ์์ ์ฝ๊ฒ ํ์ ํ ์ ์๊ธฐ ๋๋ฌธ์ ํฐ ๋ฌธ์ ๊ฐ ๋์ง ์๋๋ค๊ณ ๋งํ๋ค.
๐ฌ ๋ฌธ์ ์ํฉ 4. ์ธ๋ถ ์์๋ ์ ๋๋๋ฐ, ํตํฉ ํ ์คํธ๋ ์ ๋ ๋
๋ง์ฝ, CrewService์์ canGraduate() ๋ฉ์๋๊ฐ ๋ค์๊ณผ ๊ฐ์ด ์คํ๋๋ค๊ณ ๊ฐ์ ํด๋ณด์.
ํฌ๋ฃจ๋ค์ ์๋ฃ ์ฌ๋ถ๋ฅผ ํ๋จํ๊ธฐ ์ํด์ PrologService์์ ๊ธ์ ์์ฑํ์๋์ง, JjimggongService์์ ์์ฝ์ ํ ์ ์๋์ง, MissionService์์ ๋ชจ๋ ๋ฏธ์ ์ ํต๊ณผํ๋์ง ํ๋จํ๋ ๋ก์ง์ ํธ์ถํ๋ค.
classicist๋ ๋ค์๊ณผ ๊ฐ์ ํ๋ก์ฐ๋ก ์งํํ๋ค.
๐ฌ CrewService์์ ์กธ์ ๊ฐ๋ฅ ์ฌ๋ถ๋ฅผ ํ๋จํ๋ ํ ์คํธ๊ฐ ํ์ํด!
1. PrologService์์ ๊ธ ์์ฑ ์ฌ๋ถ ํ๋จ ๋ฉ์๋ ์์ฑ
2. JjimggonService์์ ์์ฝ ์ฌ๋ถ ํ๋จ ๋ฉ์๋ ์์ฑ
3. MissionService์์ ๋ชจ๋ ๋ฏธ์ ํต๊ณผ ์ฌ๋ถ ํ๋จ ๋ฉ์๋ ์์ฑ
4. CrewService์์ ํ ์คํธ ์งํ!
ํ์ง๋ง, classicist๋ ๋น์ฅ ํ ์คํธ๋ฅผ ์งํํ๊ณ ์๋ CrewService์ canGraduate() ๋ฉ์๋์ ํ ์คํธ์๋ง ์ง์คํ๊ณ ์์์ ๊ฒ์ด๋ค. ๊ทธ๋ ๊ธฐ ๋๋ฌธ์ ์ค๊ฐ์ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ์ง ์์๊ฑฐ๋, ํน์ ์ ๋๋ก ํ์ธํ์ง ๋ชปํ๋ค๋ฉด ์ค๋ฅ๊ฐ ๋ฐ์ํ๋๋ผ๋ ๋์ด๊ฐ์ ์๋ ์๋ค.
์ฆ, classicist์์๋ ์ด๋ฌํ ํ๋ ฅ ๊ฐ์ฒด์ ๋ํด์๋ ๊ผผ๊ผผํ ํ ์คํธ๊ฐ ํ์ํ๋ค.
๋ฐ๋ฉด์, mockist๋ ๋ค์๊ณผ ๊ฐ์ด ์งํํ๋ค.
1. PrologService์์ ๊ธ ์์ฑ ์ฌ๋ถ ํ๋จ ๋ฉ์๋์ ๋ํ mock ๊ฐ์ฒด ์์ฑ
2. JjimggonService์์ ์์ฝ ์ฌ๋ถ ํ๋จ ๋ฉ์๋์ ๋ํ mock ๊ฐ์ฒด ์์ฑ
3. MissionService์์ ๋ชจ๋ ๋ฏธ์ ํต๊ณผ ์ฌ๋ถ ํ๋จ mock ๊ฐ์ฒด ์์ฑ
4. CrewService์์ ํ ์คํธ ์๋ฃ!
5. ๊ฐ๊ฐ์ ๋ํ ์ธ๋ถ ๊ตฌํ ์งํ
ํ๋ ฅ ๊ฐ์ฒด๋ฅผ ๊ตฌํํ๊ธฐ ์ ๋ถํฐ mock ๊ฐ์ฒด๋ฅผ ์์ฑํด๋๊ธฐ ๋๋ฌธ์ ๊ธฐ๋ณธ์ ์ผ๋ก ๊ธฐ๋ฅ์ ๊ตฌํํด์ผ ํ๋ ๋ชฉ๋ก๋ค์ ๋ํด ๋ ์ ์ธ์ํ๊ฒ ๋๋ค.
Mockist๋ ๊ธฐ๋ณธ์ ์ผ๋ก ํ๋ ฅ ๊ฐ์ฒด๋ฅผ ๊ตฌํํ๊ธฐ ์ ์ SUT์ ๋ํ ํ ์คํธ๋ฅผ ์์ฑํ์ง๋ง, classicist๋ SUT์ ๋ํ ๊ธฐ๋ฅ์ ๋ชจ๋ ๊ตฌํํ ๋ค์์ ํ๋ ฅ ๊ฐ์ฒด์ ๋ํด ํ ์คํธ๋ฅผ ์งํํ์ฌ ๋์น ํ๋ฅ ์ด ๋๋ค๋ ๊ฒ์ด๋ค.
โ๏ธ Inside-Out vs Outside-In
classicist๋ inside-out์, mockist๋ outside-in์ ํ์ฉํด์ ๊ฐ๋ฐ์ ์งํํ๋ค.
์ ์๋ ํ๋์ ๊ธฐ๋ฅ์ ๊ตฌํํ ๋ ๋ด๋ถ ๊ตฌ์ฑ ์์๋ค์ ํ๋์ฉ ๊ตฌ์ฑํด๋๊ฐ๋ฉด์ (๋๋ฉ์ธ๋ถํฐ ์ค๊ณ) ๊ฐ์ฅ ๋ง์ง๋ง์ controller๋ฅผ ์์ฑํด ์ฌ์ฉ์์๊ฒ ์ ๊ณตํ๋ ๋ฐฉ๋ฒ์ด๊ณ , ํ์๋ UI์ ๊ฐ์ฅ ๊ฐ๊น์ด ๊ณ์ธต๋ถํฐ ์์ํ์ฌ ๋๋ฉ์ธ๊น์ง ๋ด๋ ค๊ฐ๋ ๋ฐฉ์์ด๋ค.
Inside-out์ ๊ฒฝ์ฐ ์ด๋ค ๋๋ฉ์ธ๋ถํฐ ์์ํ๋ฉด ์ข์์ง ๊ฐํผ๋ฅผ ์ก๊ธฐ ์ด๋ ค์ด ๊ฒฝ์ฐ๊ฐ ๋ง๋ค.
๊ทธ๋ฌ๋, ๋ฆฌํฉํฐ๋ง์ ์งํํ๋ฉด์ ์ฃผ๋ก ๊ฐ์ฒด์ ๋ํ ์ฑ ์ ๋ถ๋ฆฌ๋ฅผ ์งํํ ๋ ๋ณด๋ค ๋์ ๋๋ฉ์ธ ๋ชจ๋ธ์ด ๋์ถ๋ ์๋ ์๊ณ , ์์ ์์๋ถํฐ ์์ํ๊ธฐ ๋๋ฌธ์ ์ค๋ฒ ์์ง๋์ด๋ง๋ ๋น๊ต์ ํผํ ์ ์๋ค. ๋ํ, ๋ด๋ถ ์์์ ๋ํ ๊ตฌํ์ ์์ฃผ๋ก ์งํํ๋ค ๋ณด๋๊น ๊ธฐ๋ฅ์ ์ผ๋ก ์์ ์ฑ์ ๋ณด์์ ๋ ์ด๊ธฐ์๋ ๋ ์ ๋ฆฌํ ์๋ ์์ ๊ฒ ๊ฐ๋ค. ๊ทธ๋ฌ๋, ํ๋ ฅ ๊ด๊ณ๋ฅผ ์ ๋๋ก ์ธ์ฐ์ง ์์ผ๋ฉด ์๋ชป๋ ํ ์คํธ๋ฅผ ์์ฑํ ์๋ ์๋ค๋ ๊ฒ์ด ๋จ์ ์ด๋ค. (์์์ ๋งํ๋ ๊ฒ์ฒ๋ผ ๋นผ๋จน๋๋ค๋ ์ง... ๊ทธ๋ฐ ๊ฒฝ์ฐ๊ฐ ์๊ธธ ์๋ ์์ผ๋๊น)
๋ฐ๋ฉด, Outside-in์ ๊ฒฝ์ฐ ํ๋ ฅ ๊ฐ์ฒด ์์ฒด๊ฐ ๊ตฌํ๋์ง ์์ ์ํ์์ ์งํํ๊ธฐ ๋๋ฌธ์, ์ด๋ค ๊ฐ์ฒด๊ฐ ์ด๋ค ๊ฒ์ ์์กดํ๋์ง, ์ํธ ์์ฉ์ ์ด๋ค ์์ผ๋ก ํ๋์ง์ ๋ ์ง์คํ๊ฒ ๋๋ค. ์ฌ์ฉ์์๊ฒ ๊ฐ์ฅ ๋ง๋ฟ์ ์์ญ๋ถํฐ ์งํํ๊ธฐ ๋๋ฌธ์ '์ฌ์ฉ์์ ์์ฒญ์ด ๋ค์ด์์ ๋, ๋ฌด์์ ์ ๊ณตํด์ผ ํ์ง?'๋ผ๋ ๋ฌผ์์ผ๋ก ์์ํ์ฌ, ๋ด๋ถ ํ๋ ฅ ๊ฐ์ฒด์ ๊ตฌ์ฑ์ ๋ํด์ ๊ณ ๋ฏผํ๊ฒ ๋๋ค. ์ด์ฐจํผ ๊ตฌํ๋์ง ์์ ๋ถ๋ถ์ Mock์ผ๋ก ์ค๊ณํ๋ฉด ๋๋ ๊ฒ์ด๋๊น. ์ด ๋๋ถ์ ๊ฐ์ฒด์ ์ธ๋ถ ๊ตฌํ๋ณด๋ค๋ ํ์์ ์ง์คํ๊ฒ ๋๊ณ , ์ฑ ์์ ๋ํด ๋ ๋ช ํํ๊ฒ ๋ํ๋ผ ์ ์์ด์ ๊ฐ์ฒด์งํฅ์ ์ธ ์ฝ๋ ์์ฑ์ด ๊ฐ๋ฅํ ์ ์๋ค. ํ์ง๋ง ์ฑ ์ ๋ถ๋ฆฌ์ ์ต์ํ์ง ์๋ค๋ฉด ์คํ๋ ค ์ค๋ฒ ์์ง๋์ด๋ง์ ๊ฐ์ง ์๋ ์๋ค๋ ๊ฒ ๋จ์ ์ด๋ค.
classicist | mockist | |
ํ ์คํธ ๋จ์ | sociable test | solitary test |
ํ ์คํธ ๊ฒ์ฆ | ์ํ ๊ฒ์ฆ | ํ์ ๊ฒ์ฆ |
ํ๋ ฅ ๊ฐ์ฒด ์ค๋น | Fixture ๊ตฌ์ฑ ๋ฐ ์ฌ์ฌ์ฉ | ๋ฉ์๋๋ง๋ค Mock ์์ฑ ๋ฐ ๊ธฐ๋๊ฐ ์ค์ |
๊ฐ๋จํ ํ๋ ฅ ๊ฐ์ฒด | ์ค์ ๊ฐ์ฒด ์ฌ์ฉ | ํ ์คํธ ๋๋ธ ์ ๊ทน ์ฌ์ฉ |
์ด๋ ค์ด ํ๋ ฅ ๊ฐ์ฒด | ํ ์คํธ ๋๋ธ ์ฌ์ฉ ๊ณ ๋ ค | ํ ์คํธ ๋๋ธ ์ ๊ทน ์ฌ์ฉ |
ํ ์คํธ ์ ์ง ๋ณด์ | Fixture์ ๋ณ๊ฒฝ์ด ์์ ๋ ํ ์คํธ๊ฐ ๊นจ์ง๋ค | ๊ตฌํ ํ ์คํธ๋ก ์ด์ด์ ธ์ ํ ์คํธ ์์ ์ ๋ง์ ์๊ฐ์ด ์์๋ ์ ์๋ค |
ํ ์คํธ ๊ฒฉ๋ฆฌ | X (๋๋ฒ๊ทธ๊ฐ ํ๋ค๋ค) | O (ํตํฉ ํ ์คํธ ํ์) |
๋์์ธ ์คํ์ผ | Inside-out | Outside-in |
์ด ๋ง์ ๋ฃ๊ณ ์ผ์ข ์ DDD์ ๋น์ทํ ๊ฒ์ผ๊น? ๋ผ๋ ์๊ฐ๋ ํ์๋๋ฐ,
์๊ฐํด๋ณด๋ฉด DDD๋ ๋๋ฉ์ธ ์ฃผ๋ ๊ฐ๋ฐ์ด๊ณ , ์ฐ๋ฆฌ๊ฐ ๋งํ๋ classicist์ Mockist๋ TDD - ์ฆ, ํ ์คํธ ์ฃผ๋ ๊ฐ๋ฐ์ด๋ค.
์์ ๋ค๋ฅธ ๊ฐ๋ฐ ๋ฐฉ๋ฒ๋ก ์ด์ง๋ง, ๋ง์ฝ DDD๋ฅผ ์งํํ๋ฉด์ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ๊ฒ ๋๋ค๋ฉด ์ด๋ classicist์ ์กฐ๊ธ ๋ ๊ฐ๊น์ด ํํ๊ฐ ์๋๊น ์ถ๋ค. (๋๋ฉ์ธ ํ ์คํธ -> ์์ฑ... ์ด๋ฐ ๋๋ ์๋๊น?)
ํ ์คํธ ์ฝ๋๋ฅผ ์ด์ฌํ ์์ฑํด์ผ๊ฒ ๋ค... ๋!