[TDD Chapter9] ํ ์คํธ ๋ฒ์์ ์ข ๋ฅ
๐ฅ ํ ์คํธ ๋ฒ์์ ์ข ๋ฅ
์๋ฅผ ๋ค์ด, ๊ฐ๋ฐ ์๋ฃ ํ์ ์งํํ๋ ํ ์คํธ๋ฅผ ํตํฉ ํ ์คํธ๋ผ๊ณ ๋ถ๋ฅด๋๋ฐ, ๊ณ ๊ฐ ์ ์ฅ์์ ์๊ตฌํ ๊ธฐ๋ฅ์ ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํํ๋์ง ์ํํ๋ ํ ์คํธ๋ฅผ ์ธ์ ํ ์คํธ๋ผ๊ณ ๋ถ๋ฅธ๋ค. ์ด์ฒ๋ผ, ํ ์คํธ์ ๊ด๋ จ ์ฉ์ด๋ ๋ฌธ๋งฅ์ด๋ ์ฌ์ฉ์์ ๋ฐ๋ผ ์๋ฏธ๊ฐ ๋ค๋ฅผ ์ ์๋ค.
๐ป ๊ธฐ๋ฅ ํ ์คํธ(Functional Test) ์ E2E ํ ์คํธ
์ฌ์ฉ์ ์ ์ฅ์์ ์์คํ ์ด ์ ๊ณตํ๋ ๊ธฐ๋ฅ์ด ์ฌ๋ฐ๋ฅด๊ฒ ๋์ํ๋์ง ํ์ธํ๋ค. ์ด ํ ์คํธ๋ฅผ ์ํด์๋ ์์คํ ์ ๊ตฌ๋ํ๊ณ ์ฌ์ฉํ๋๋ฐ ํ์ํ ๋ชจ๋ ๊ตฌ์ฑ์์๊ฐ ํ์ํ๋ค.
- ์ฌ์ฉ์๊ฐ ์ง์ ์ฌ์ฉํ๋ ์น ๋ธ๋ผ์ฐ์ ๋ ๋ชจ๋ฐ์ผ ์ฑ๋ถํฐ ์์ํ์ฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ์ธ๋ถ ์๋น์ค์ ์ด๋ฅด๊ธฐ๊น์ง ๋ชจ๋ ๊ตฌ์ฑ ์์๋ฅผ ํ๋๋ก ์ฎ์ด ์งํ
- ๋ธ๋ผ์ฐ์ (๋) ~ ๋ฐ์ดํฐ๋ฒ ์ด์ค(๋)๊น์ง ๋ชจ๋ ๊ตฌ์ฑ์์๋ฅผ ์์ ํ ๋ ผ๋ฆฌ์ ์ผ๋ก ํ๋์ ๊ธฐ๋ฅ์ผ๋ก ๋ค๋ฃธ
- ์ด์ END TO END (E2E) ํ ์คํธ๋ผ๊ณ ๋ถ๋ฆฌ๊ธฐ๋ ํจ.
- QA ์กฐ์ง์์ ์ฃผ๋ก ํ๋ ํ ์คํธ
- ์์คํ ์ด ํ์ํ๋ ๋ฐ์ดํฐ ์ ๋ ฅํ๊ณ ๊ฒฐ๊ณผ๊ฐ ์ฌ๋ฐ๋ฅธ์ง ํ์ธ
- ์ค์ํ ๊ฒ์, ์ฌ์ฉ์์ ์ ์ฅ์์ ๋ชจ๋ ๊ธฐ๋ฅ์ ํ์ธํ๋ค๋ ๊ฒ์ด๋ค.
๐ป ํตํฉ ํ ์คํธ
์์คํ ์ ๊ฐ ๊ตฌ์ฑ์์๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ์ฐ๋๋๋์ง ํ์ธํ๋ ๊ฒ์ด๋ค. ์ฌ์ฉ์์ ์ ์ฅ์์ ์ฝ๋๋ฅผ ํ ์คํธํ๋ ๊ฒ์ด ์๋, ์ง์ ์ํํธ์จ์ด์ ์ฝ๋๋ฅด ์ง์ ํ ์คํธํ๋ค.
- ์ผ๋ฐ์ ์ธ ์น ์ดํ๋ฆฌ์ผ์ด์ ์ ํ๋ ์์ํฌ, ๋ผ์ด๋ธ๋ฌ๋ฆฌ, ๋ฐ์ดํฐ๋ฒ ์ด์ค, ๊ตฌํํ ์ฝ๋๊ฐ ์ฃผ์ ํตํฉ ํ ์คํธ ๋์
๐ป ๋จ์ ํ ์คํธ
๊ฐ๋ณ ์ฝ๋๋ ์ปดํฌ๋ํธ๊ฐ ๊ธฐ๋ํ ๋๋ก ๋์ํ๋์ง ํ์ธํ๋ค. ์ฌํ๊น์ง ๋ธ๋ก๊ทธ์ ํฌ์คํ ํ ์ฝ๋๋ ์ฃผ๋ก ๋จ์ ํ ์คํธ ์ฝ๋์ด๋ค.
- ํ ํด๋์ค๋ ํ ๋ฉ์๋์ ๊ฐ์ด ์์ ๋ฒ์๋ฅผ ํ ์คํธํ๋ค.
- ์ผ๋ถ ์์กด ๋์์ ์คํ ์ด๋ ๋ชจ์ ๊ฐ์ฒด๋ฑ์ ์ด์ฉํด์ ๋์ญ์ผ๋ก ๋์ฒดํ๋ค.
๐ป ํ ์คํธ ๋ฒ์๊ฐ ์ฐจ์ด
ํตํฉ ํ ์คํธ | ๊ธฐ๋ฅ ํ ์คํธ | ๋จ์ ํ ์คํธ |
DB๋ ์บ์ ์๋ฒ์ ๊ฐ์ ์ฐ๋ ๋์์ ๊ตฌ์ฑ, ํ ์คํธ ์๋๋ฆฌ์ค๋ฅผ ์ํด ๋ง์ ๋ ธ๋ ฅ | ์น ์๋ฒ๋ฅผ ๊ตฌ๋ํ๊ฑฐ๋ ๋ชจ๋ฐ์ผ ์ฑ์ ํฐ์ ์ค์น, ํ ์คํธ ์๋๋ฆฌ์ค๋ฅผ ์ํด ๋ง์ ๋ ธ๋ ฅ | ํ ์คํธ ์ฝ๋๋ง ๋นผ๋๋ฉด ๋จ |
DB ์ฐ๊ฒฐ, ์์ผ ํต์ , ์คํ๋ง ์ปจํ ์ด๋ ์ด๊ธฐํ ๋ฑ๊ณผ ๊ฐ์ด ์คํ ์๋ ๋๋ฆฌ๊ฒ ํ๋ ์์ธ์ด ๋ง์ | ๋ธ๋ผ์ฐ์ ๋ ์ฑ์ ๊ตฌ๋ํ๊ณ ํ๋ฉด์ ํ๋ฆ์ ๋ฐ๋ผ ์๋ง์ ์ํธ์์ฉ | ์๋ฒ๋ฅผ ๊ตฌ๋ํ๊ฑฐ๋ DB๋ฅผ ์ค๋นํ ํ์๊ฐ ์์, ์์กด ๋์์ ๋์ญ์ผ๋ก ๋์ฒด |
ํตํฉ ํ ์คํธ๋ ๊ธฐ๋ฅ ํ ์คํธ๋ก๋ ๊ฒฐ๊ณผ ํ์ธ์ด ์ด๋ ค์ธ ๋, ๋จ์ ํ ์คํธ์ ๋์ญ์ ์กฐํฉํ์ฌ ์ํฉ์ ๋ง๋ค๊ณ ๊ฒฐ๊ณผ๋ฅผ ํ์ธํจ. |
ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ๋ ๊ฐ๋ฐ์๋ ๋จ์ ํ ์คํธ์ ํตํฉ ํ ์คํธ๋ฅผ ์์ด์ ์์ฑํ๋ค. ์ด๋ ํ ์คํธ๋ฅผ ๋ ๋ง์ด ์์ฑํด์ผํ๋ค๋ ๊ท์น์ ์์ง๋ง, ์ฃผ๋ก ๋จ์ ํ ์คํธ๋ฅผ ๋ง์ด ์์ฑํ๋ ํธ์ด๋ผ๊ณ ํ๋ค.
๋จ์ํ ์คํธ๋ฅผ ์ฃผ๋ก ์ฌ์ฉํ๋ค๊ณ ๋ ํ์ง๋ง, ๊ฐ ๊ตฌ์ฑ ์์๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ์ฐ๋๋๋์ง ํ์ธํ๊ธฐ ์ํด์ ํตํฉ ํ ์คํธ๋ ํ์ํ๋ค.
๐ป ํ ์คํธ ๋ฒ์์ ํ๋ฅธ ํ ์คํธ ์ฝ๋ ๊ฐ์์ ์๊ฐ
๊ธฐ๋ฅ, ํตํฉ, ๋จ์ ํ ์คํธ ๋ฑ ์ ๋ฒ์์ ๋ํด ํ ์คํธ๋ฅผ ์๋ํํ๋ ์๋๊ฐ ์ฆ๊ฐํ๊ณ ์์ผ๋ฉฐ, ์ด๋ฅผ ํตํด ๊ณ ํ์ง์ ์ํํธ์จ์ด๋ฅผ ๋ ๋น ๋ฅด๊ฒ ์ถ์ํ๊ณ ์ํ๋ ์ ๋ต์ด ๋์ด๋๊ณ ์๋ค.
๊ธฐ๋ฅ ํ ์คํธ๋ ํ ์คํธํ๊ธฐ์ํ ๋ชจ๋ ํ๊ฒฝ์ด ๊ฐ์ถฐ์ ธ ์์ด์ผ ํ๊ธฐ ๋๋ฌธ์, ์๋ํํ๊ฑฐ๋ ๋ค์ํ ์ํฉ๋ณ๋ก ํ ์คํธํ๊ธฐ ๊ฐ์ฅ ์ด๋ ต๋ค. ๋ฐ๋ผ์, ์ ์์ ์ธ ๊ฒฝ์ฐ์ ๋ช ๊ฐ์ง ํน์ํ ์ํฉ์ ํ ์คํธ ๋ฒ์๋ฅผ ์ก์ ํ ์คํธ๋ฅผ ์งํํ๋ค.
ํตํฉ ํ ์คํธ๋ ๊ธฐ๋ฅ ํ ์คํธ์ ๋นํด ์ ์ฝ์ด ๋ํ๋ค. ๊ธฐ๋ฅ ํ ์คํธ์ ๋นํด ์๋์ ์ผ๋ก ์คํ ์๊ฐ์ด ์งง๊ณ ์ํฉ๋ณด๋ค ์ ์ฐํ๊ฒ ๊ตฌ์ฑํ ์ ์๊ธฐ ๋๋ฌธ์ ๋ณดํต ๊ธฐ๋ฅ ํ ์คํธ๋ณด๋ค ํตํฉ ํ ์คํธ๋ฅผ ๋ ๋ง์ด ์ฌ์ฉ๋๋ค.
๋จ์ ํ ์คํธ๋ ํตํฉ ํ ์คํธ๋ก๋ ๋ง๋ค๊ธฐ ํ๋ ์ํฉ์ ์ฝ๊ฒ ๊ตฌ์ฑํ ์ ์๋ค. ์์ ๋จ์๋ฅผ ๋์์ผ๋ก ํ ์คํธ ์ฝ๋๋ฅด ๋ง๋ค๊ณ ๋ ๋ค์ํ ์ํฉ์ ๋ค๋ฃจ๊ธฐ ๋๋ฌธ์ ํตํฉ ํ ์คํธ๋ณด๋ค ๋ ๋ง์ด ์ฌ์ฉ๋๋ค.
๊ธฐ๋ฅ, ํตํฉ์์ ๋ชจ๋ ์์ธ ์ํฉ์ ํ ์คํธํ๋ฉด ํ ์คํธ๋ฅผ ๋ค๋ฃจ๋ ๋ด์ฉ์ด ์ค๋ณต๋๊ธฐ ๋๋ฌธ์ ๋จ์ ํ ์คํธ๋ ์ค์ด๋ ๋ค. ํ ์คํธ ์๋๋ ๋จ์ ํ ์คํธ๊ฐ ๋ค๋ฅธ ํ ์คํธ๋ค๋ณด๋ค ์๋๊ฐ ์๋ฑํ ๋๊ธฐ ๋๋ฌธ์ ๊ฐ๋ฅํ๋ฉด ๋จ์ ํ ์คํธ์ ๋ค์ํ ์ํฉ์ ๋ค๋ฃจ๊ณ , ํตํฉ๊ณผ ๊ธฐ๋ฅ์์๋ ์ฃผ์ ์ํฉ์ ์ด์ ์ ๋ง์ถฐ ์ฝ๋๋ฅผ ์์ฑํด์ผํ๋ค.
์๋๊ฐ ๋๋ ค์ง๋ฉด, ์ํํธ์จ์ด ํ์ง ์ ํ๋ก ์ด์ด์ง ์ ์๊ธฐ ๋๋ฌธ์ ๊ฐ๋ฅํ๋ฉด ๋น ๋ฅธ ์๊ฐ ๋ด์ ํ ์คํธ๋ฅผ ์คํํ ์ ์๋๋ก ํด์ผํ๋ค.
๐ฅ ์ธ๋ถ ์ฐ๋์ด ํ์ํ ํ ์คํธ ์
์ํํธ์จ์ด๋ ๋ค์ํ ์ธ๋ถ ์ฐ๋์ด ํ์ํ๋ค. ์ธ๋ถ ์ฐ๋ ๋์์ ์ฝ๊ฒ ์ ์ดํ ์ ์๊ธฐ ๋๋ฌธ์ ์ฐ๋ํด์ผ ํ ๋์์ด ๋์ด๋ ์๋ก ํตํฉ ํ ์คํธ๋ ํ๋ค์ด์ง๋ค.
๋ชจ๋ ์ธ๋ถ ์ฐ๋ ๋์์ ํตํฉ ํ ์คํธ์์ ๋ค๋ฃฐ ์ ์์ง๋ง, ์ผ๋ถ ์ธ๋ถ ๋์์ ์ด๋์ ๋ ์์ค์์ ์ ์ด๊ฐ ๊ฐ๋ฅํ๋ค.
๐ป ์คํ๋ง ๋ถํธ์ DB ํตํฉ ํ ์คํธ
Spring Boot, Spring Data JPA, MySQL ๊ธฐ์ ์ ์ฌ์ฉํ๋ค.
/**
* ํตํฉ ํ
์คํธ
*/
@SpringBootTest
@RequiredArgsConstructor
public class UserRegisterTest {
private final UserRegister register;
private final JdbcTemplate jdbcTemplate;
@Test
void ๋์ผํ_ID๊ฐ_์ด๋ฏธ_์กด์ฌํ๋ฉด_์ต์
์
(){
// ์ํฉ insert ์ฟผ๋ฆฌ ์คํ
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"));
}
@Test
void ์กด์ฌํ์ง_์์ผ๋ฉด_์ ์ฅํจ(){
// ์ํฉ delete ์ฟผ๋ฆฌ ์คํ
jdbcTemplate.update("delete from user where id = ?", "cbk");
// ์คํ
register.register("cbk", "strongpw", "email@email.com");
// ๊ฒฐ๊ณผ ํ์ธ select ์ฟผ๋ฆฌ ์งํ
SqlRowSet rs = jdbcTemplate.queryForRowSet(
"select * from user where id = ?", "cbk"
);
rs.next();
assertEquals("email@email.com", rs.getString("email"));
}
}
/**
* ๋จ์ ํ
์คํธ
*/
@SpringBootTest
public class UserRegisterTest {
private UserRegister userRegister;
private MemoryUserRepository fakeRepository = new MemoryUserRepository();
// ์๋ต
@Test
void ์ด๋ฏธ_๊ฐ์_ID_์กด์ฌํ๋ฉด_๊ฐ์
_์คํจ(){
// ๋จ์ ํ
์คํธ๋ **๋์ญ**์ ์ด์ฉํ ์ํฉ ๊ตฌ์ฑ
fakeRepository.save(new User("id", "pw1", "email@email.com"));
assertThrows(DupIdException.class, () ->{
userRegister.register("id", "pw2", "email");
});
}
}
ํตํฉ ํ ์คํธ๋ ์ค์ ๋ก DB๋ฅผ ์ฌ์ฉํ๋ค. ๋์ผํ ํ ์คํธ๋ฅผ ์ฌ๋ฌ๋ฒ ์คํํด๋ ๊ฒฐ๊ณผ๊ฐ ๊ฐ๊ฒ ๋์์ผํ๋ฏ๋ก ํ ์คํธ์ฝ๋์์ DB ๋ฐ์ดํฐ๋ฅผ ์๋ง๊ฒ ์ ์ดํด์ผ ํ๋ค.
๐ป WireMock์ ์ด์ฉํ REST ํด๋ผ์ด์ธํธ ํ ์คํธ
ํตํฉ ํ ์คํธํ๊ธฐ ์ด๋ ค์ด ๋์์ด ์ธ๋ถ ์๋ฒ์ด๋ค.
public class CardNumberValidator {
// 7์ฅ๊ณผ ๋ฌ๋ฆฌ ์ถ๊ฐ๋ ์ฝ๋
// ์์ฑ์๋ก๋ถํฐ server url ์ ๋ฐ์ ํ๋์ฝ๋ฉ์ด ๋์ง ์๋๋ก ํจ.
private String server;
public CardNumberValidator(String server){
this.server = server;
}
//
public CardValidity validate(String cardNumber){
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(server + "/card"))
.header("Content-Type", "text/plain")
.POST(HttpRequest.BodyPublishers.ofString(cardNumber))
.build();
try{
HttpResponse<String> response =
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
switch (response.body()){
case "ok" : return CardValidity.VALID;
case "bad" : return CardValidity.INVALID;
case "expired" : return CardValidity.EXPIRED;
case "theft" : return CardValidity.THEFT;
default : return CardValidity.UNKNOWNS;
}
}catch (HttpTimeoutException e){
return CardValidity.TIMEOUT;
}
catch (IOException | InterruptedException e){
return CardValidity.ERROR;
}
}
}
์ ํด๋์ค ์์ฒด๋ฅผ ํ ์คํธํ๋ ค๋ฉด ์ ํด์ง ๊ท์น์ ๋ง๊ฒ ํต์ ํ ์ ์๋ ์๋ฒ๊ฐ ํ์ํ๋ฐ, ์ธ๋ถ์ ์นด๋ ์ ๋ณด ์ ๊ณต api์ ํต์ ํ ๋ ์ํ๋ ์ํฉ์ ์ฝ๊ฒ ๋ง๋ค ์ ์๋ค. ์ด ๋, ์ฌ์ฉํ๋ ๊ฒ์ด WireMock์ด๋ค. WireMock์ ์ฌ์ฉํ์ฌ ์ฌ๋ฐ๋ฅธ ์๋ต์ด๋ ํ์์์๊ณผ ๊ฐ์ ์ํฉ์ ๋ํด ํ ์คํธ ํ ์ ์๋ค.
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")) // url, post ์์ฒญ
.withRequestBody(equalTo("1234567890")) // ์์ฒญ ๋ชธ์ฒด๊ฐ "1234567890 ๊ณผ ์์ฒญ์ด ๊ฐ์ผ๋ฉด
.willReturn(aResponse()
.withHeader("Content-Type", "text/plain") // header๋ ๋ค์๊ณผ ๊ฐ์ด
.withBody("ok"))); // ์๋ต ๋ชธ์ฒด๊ฐ 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);
}
}
WireMockServer
- ํ ์คํธ ์คํ ์ ์์, ์ค์ HTTP ์๋ฒ๊ฐ ๋ฌ๋ค
- ํ ์คํธ์์ WireMockServer์ ๋์์ ๊ธฐ์
- HTTP ์ฐ๋์ ์ํํ๋ ํ ์คํธ ์คํ
- ํ ์คํธ ์คํ ํ์ WireMockServer ์ค์ง
๐ป ์คํ๋ง๋ถํธ ๋ด์ฅ ์๋ฒ๋ฅผ ์ด์ฉํ API ๊ธฐ๋ฅ ํ ์คํธ
๋ชจ๋ฐ์ผ ์ฑ์์ ํ์ ๊ฐ์ ์ ์ํด ์ฌ์ฉํ๋ ํ์ ๊ฐ์ API๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ JSON์ ์๋ตํ๋์ง ๊ฒ์ฆํ๋ ์ฝ๋๋ฅผ ์์ฑํด๋ณด์.
ํ์ ๊ฐ์ ์ ๋งค์ฐ ์ค์ํ๊ธฐ ๋๋ฌธ์ ํ์ ๊ฐ์ API๋ฅผ ๊ฒ์ฆํ๋ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ์ฌ ๊ฒ์ฆ ๊ณผ์ ์ ์๋ํํ๋ฉด ๋ด๋น์๊ฐ ์๋์ผ๋ก ํ ์คํธํ๋ ์๊ฐ์ ์ค์ผ ์ ์๋ค.
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
@RequiredArgsConstructor
public class UserApiE2Test {
private final TestRestTemplate testRestTemplate;
@Test
void weakPwResponse(){
String reqBody = "{\"id\":\"id\", \"pw\":\"123\",\"email\":\"a@a.com\"}";
RequestEntity<String> request = RequestEntity.post(URI.create("/users"))
.contentType(PageAttributes.MediaType.APPLICATION_JSON_UTP8)
.body(reqBody);
ResponseEntity<String> response = testRestTemplate.exchange(
request, String.class
);
assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode());
assertTrue(response.getBody().contains("WeakPasswordException"));
}
}
TestRestTemplate์ ์คํ๋ง ๋ถํธ๊ฐ ํ ์คํธ ๋ชฉ์ ์ผ๋ก ์ ๊ณตํ๋ ๊ฒ์ผ๋ก์, ๋ด์ฅ ์๋ฒ์ ์ฐ๊ฒฐํ๋ RestTemplate์ด๋ค. ์์์ ํฌํธ๋ฅผ ์ฌ์ฉํ์ฌ ๋ด์ฅ์๋ฒ๋ฅผ ๊ตฌ๋ํ๋๋ก ์ค์ ์ ํด๋์๋ค.