깨끗한 테스트 코드
목차
1. 테스트 코드의 중요성
2. 테스트의 종류
3. Unit Test 작성
4. FIRST 원칙
5. 오픈소스 속 Unit Test
1. 테스트 코드의 중요성
- 테스트 코드는 실수를 바로잡아준다.
- 테스트 코드는 반드시 존재해야하며, 실제 코드 못지 않게 중요하다.
- 테스트 케이스는 변경이 쉽도록 한다. 코드에 유연성, 유지보수성, 재사용성을 제공하는 버팀목이 바로 단위테스트다.
- 테스트 케이스가 있으면 변경이 두렵지 않다. 테스트 케이스가 없다면 모든 변경이 잠정적인 버그다. 테스트 커버리지가 높을수록 버그 에 대한 공포가 줄어든다.
- 지저분한 테스트 코드는 테스트를 안하니만 못하다.
<Effective Unit Test 책에서>
'테스트는 실사용에 적합한 설계를 끌어내준다.'
'테스트를 작성해서 얻게 되는 가장 큰 수확은 테스트 자체가 아니다. 작성 과정에서 얻는 깨달음이다.'
'테스트트 자동화되어야 한다.' - 매번 배포할 때마다 실행되어야 한다.
2. 테스트의 종류
Unit Test : 프로그램 내부의 개발 컴포넌트의 동작을 테스트한다. 배포하기 전에 자동으로 실행되도록 많이 사용한다.
Integration Test : 프로그램 내부의 개별 컴포넌트들을 합쳐서 동작을 테스트한다. Unit Test는 각 컴포넌트를 고립시켜 테스트하기 때문에 컴포넌트의 interaction을 확인하는 Integration Test가 필요하다.
E2E Test : End to End Test. 실제 유저의 시나이로대로 네트워크를 통해 서버의 Endpoint를 호출해 테스트한다.
3. Unit Test 작성
'테스트 라이브러리를 사용하자' (실무에서 JUnit5 + mockito를 많이 사용한다.)
Test Double
* 테스트에서 원본 객체를 대신하는 객체
Stub : - 원래의 구현을 최대한 단순한 것으로 대체한다. - 테스트를 위해 프로그래밍된 항목에만 응답한다.
Spy :
- Stub의 역할을 하면서 호출에 대한 정보를 기록한다.
- 이메일 서비스에서 메시지가 몇 번 정송되는지 확인할 때
Mock :
- 행위를 검증하기 위해 가짜 객체를 만들어 테스트하는 방법
- 호출에 대한 동작을 프로그래밍할 수 있다.
- Stub은 상태를 검증하고 Mock은 행위를 검증한다.
'given, when, then 패턴을 사용하자'
- given : 테스트를 위한 pre-condition
- when : 테스트하고 싶은 동작 호출
- then : 테스트 결과 확인
4. FIRST 원칙
Fast : 빠르게
- 테스트는 빨리 돌아야 한다. 자주 돌려야 하기 때문이다.
Independent : 독립적으로
- 각 테스트를 독립적으로 작성한다. 서로에게 의존하면 실패한 원인을 찾기 어려워진다.(다른 테스트의 샐패로 인한건지, 코드 오류인지)
Repeatable : 반복가능하게
- 테스트는 어떤 환경에서도 반복 가능해야 한다. 실제 환경, QA 환경, 모든 환경에서 돌아가야 한다.
Self-Validating : 자가검증하는
- 테스트는 bool 값으로 결과를 내야 한다.
Timely : 적시에
- 테스트하려는 실제 코드를 구현하기 직전에 구현한다.
4. 오픈소스 속 Unit Test
Trino(PrestoSQL) 프로젝트 코드
- 책에서는 하나의 테스트에 하나의 assert를 사용하라고 했다.
- 테스트하려는 인자가 많은 경우는 하나에 몰아서 하는 경우도 많다.
public class TestTypeCalculation
{
@Test
public void testBasicUsage()
{
assertEquals(Long.valueOf(42), calculateLiteralValue("42", ImmutableMap.of()));
assertEquals(Long.valueOf(0), calculateLiteralValue("NULL", ImmutableMap.of()));
assertEquals(Long.valueOf(0), calculateLiteralValue("null", ImmutableMap.of()));
assertEquals(Long.valueOf(42), calculateLiteralValue("x", ImmutableMap.of("x", 42L)));
assertEquals(Long.valueOf(42), calculateLiteralValue("(42)", ImmutableMap.of()));
assertEquals(Long.valueOf(0), calculateLiteralValue("(NULL)", ImmutableMap.of()));
assertEquals(Long.valueOf(42), calculateLiteralValue("(x)", ImmutableMap.of("x", 42L)));
assertEquals(Long.valueOf(42 + 55), calculateLiteralValue("42 + 55", ImmutableMap.of()));
assertEquals(Long.valueOf(42 - 55), calculateLiteralValue("42 - 55", ImmutableMap.of()));
assertEquals(Long.valueOf(42 * 55), calculateLiteralValue("42 * 55", ImmutableMap.of()));
assertEquals(Long.valueOf(42 / 6), calculateLiteralValue("42 / 6", ImmutableMap.of()));
assertEquals(Long.valueOf(42 + 55 * 6), calculateLiteralValue("42 + 55 * 6", ImmutableMap.of()));
assertEquals(Long.valueOf((42 + 55) * 6), calculateLiteralValue("(42 + 55) * 6", ImmutableMap.of()));
assertEquals(Long.valueOf(2), calculateLiteralValue("min(10,2)", ImmutableMap.of()));
assertEquals(Long.valueOf(10), calculateLiteralValue("min(10,2*10)", ImmutableMap.of()));
assertEquals(Long.valueOf(20), calculateLiteralValue("max(10,2*10)", ImmutableMap.of()));
assertEquals(Long.valueOf(10), calculateLiteralValue("max(10,2)", ImmutableMap.of()));
assertEquals(Long.valueOf(42 + 55), calculateLiteralValue("x + y", ImmutableMap.of("x", 42L, "y", 55L)));
}
}
code from - https://github.com/trinodb/trino/blob/master/core/trino-parser/src/test/java/io/trino/type/TestTypeCalculation.java
Junit5 Samples
- @DisplayName은 테스트 클래스나 메서드에 보여질 이름을 입력하는 것이다. 테스트의 목적을 명확하게 작정할수 있다.
- @ParameterizedTest는 하나의 테스트 메서드로 여러 가지 paramter를 테스트할 수 있다. @CsvSource의 값을 parameter로 넘 긴다.
- JUnit5가 테스트에 관한 유용한 기능을 많이 가지고 있기 때문에 실무에서 많이 사용한다.
class CalculatorTests {
@Test
@DisplayName("1 + 1 = 2")
void addsTwoNumbers() {
Calculator calculator = new Calculator();
assertEquals(2, calculator.add(1, 1), "1 + 1 should equal 2");
}
@ParameterizedTest(name = "{0} + {1} = {2}")
@CsvSource({
"0, 1, 1",
"1, 2, 3",
"49, 51, 100",
"1, 100, 101"
})
void add(int first, int second, int expectedResult) {
Calculator calculator = new Calculator();
assertEquals(expectedResult, calculator.add(first, second),
() -> first + " + " + second + " should equal " + expectedResult);
}
}
code from - https://github.com/junit-team/junit5-samples/blob/main/junit5-jupiter-starter-gradle/src/test/java/com/example/project/CalculatorTests.java