최대한 간단하게 정리하고, 세부 구현은 레포 보기
목차:
Test Double + mock vs stub
JUnit
Assertion
AssertJ + 다른것과 비교
Mockito + 다른것과 비교
BDD
TDD
Test Double
Test Double
스턴트맨을 stunt double 이라 한다.
여기서 따온 말이 test double이다.
test double은 위험한 일을 대신 해준다.
ex) 어떤 service를 테스트 할때, db와 관련된 영향은 배제해야 할 것이다.
test double은 실제 db와 관련된 역할을 대신한다.
Test Double을 예전에는 만들어 사용해야 했지만, 이제는 Mockito같은 프레임워크들이 기능을 제공하여 편하게 생성할 수 있다.
종류
크게 Dummy, Stub, Spy, Fake, Mock으로 나눈다.
Dummy
- 가장 기본적인 테스트 더블이다.
- 인스턴스화 된 객체가 필요하지만 기능은 필요하지 않은 경우에 사용한다.
- Dummy 객체의 메서드가 호출되었을 때 정상 동작은 보장하지 않는다.
- 객체는 전달되지만 사용되지 않는 객체이다.
public interface PringWarning {
void print();
}
public class PrintWarningDummy implements PrintWarning {
@Override
public void print() {
// 아무런 동작을 하지 않는다.
}
}
- 인터페이스를 이용해 테스트해야 하지만, 테스트에 필요없는 메소드 있을때 등 사용
Stub
- Dummy 객체가 실제로 동작하는 것 처럼 보이게 만들어 놓은 객체이다.
- 인터페이스 또는 기본 클래스가 최소한으로 구현된 상태이다.
- 테스트에서 호출된 요청에 대해 미리 준비해둔 결과를 바로 반환한다.
public class StubUserRepository implements UserRepository {
// ...
@Override
public User findById(long id) {
return new User(id);
}
}
Spy
- Stub의 역할을 가지면서 호출된 내용에 대해 약간의 정보를 기록한다.
- 테스트 더블로 구현된 객체에 자기 자신이 호출 되었을 때 확인이 필요한 부분을 기록하도록 구현한다.
- 실제 객체처럼 동작시킬 수도 있고, 필요한 부분에 대해서는 Stub로 만들어서 동작을 지정할 수도 있다.
public class MailingService {
private int sendMailCount = 0; // 자기 자신이 호출된 횟수를 저장하고, 반환한다
private Collection<Mail> mails = new ArrayList<>();
public void sendMail(Mail mail) {
sendMailCount++;
mails.add(mail);
}
public long getSendMailCount() {
return sendMailCount;
}
}
Fake
- 복잡한 로직이나 객체 내부에서 필요로 하는 다른 외부 객체들의 동작을 단순화하여 구현한 객체이다.
- 동작의 구현을 가지고 있지만 실제 프로덕션에는 적합하지 않은 객체이다.
public class FakeUserRepository implements UserRepository {
private Collection<User> users = new ArrayList<>();
@Override
public void save(User user) {
if (findById(user.getId()) == null) {
user.add(user);//원래라면 DB에 persist하는 로직이 있겠지만, 컬렉션에 저장함
}
}
@Override
public User findById(long id) {
for (User user : users) {
if (user.getId() == id) {
return user;
}
}
return null;
Mock
- 호출에 대한 기대를 명세하고 내용에 따라 동작하도록 프로그래밍 된 객체이다.
- mock은 위에 있던 dummy, stub, spy 처럼 동작할수있다.
이 그림을 보면 이해가 쉬움.
+
dummies는 아무런 기능을 하지 않으므로 기준이 명확하지만, 나머지는 기준이 모호하다고 한다.
spy가 fake가 되는 순간은? 이라는 질문에 정확히 언제다 라고 대답하기 어렵다.
Mock vs Stub
이 둘이 좀 다른데, 조심해야 한다.
테스트가 제대로 동작했는지를 검증하는 방식이 다르다.
//stub 기반
public void testOrderSendsMailIfUnfilled() {
Order order = new Order(TALISKER, 51);
MailServiceStub mailer = new MailServiceStub();
order.setMailer(mailer);
order.fill(warehouse);
assertEquals(1, mailer.numberSent());
}
//mock 기반
public void testOrderSendsMailIfUnfilled() {
Order order = new Order(TALISKER, 51);
Mock warehouse = mock(Warehouse.class);
Mock mailer = mock(MailService.class);
order.setMailer((MailService) mailer.proxy());
mailer.expects(once()).method("send");
warehouse.expects(once()).method("hasInventory")
.withAnyArguments()
.will(returnValue(false));
order.fill((Warehouse) warehouse.proxy());
}
//mock 기반
given(phoneBookRepository.contains(momContactName))
.willReturn(false);
phoneBookService.register(xContactName, "");
then(phoneBookRepository)
.should(never())
.insert(momContactName, momPhoneNumber);
//둘 다 사용
@Test
public void whenUpdatingItemItShouldUseTheGivenID() {
//given
given(repository.getOne(CHECKED_ITEM_ID)).willReturn(CHECKED_ITEM);
//when
controller.updateItem(NEW_ITEM, CHECKED_ITEM_ID);
//then
verify(repository).saveAndFlush(anyItem.capture());
assertThat(anyItem.getValue().getId()).isEqualTo(CHECKED_ITEM_ID);
}
stub
- 상태 검증(state verification)을 사용한다
- 테스트의 입력에 집중하는 경우 사용한다
- 입력값에 따라 리턴하는 결과 / exception 에 집중하는 경우
mock
- 행동 검증(behavior verification)을 사용한다
- 테스트의 출력 / 결과 에 집중하는 경우 사용
- 정상적으로 호출되었는지가 더 중요할때
테스팅 시 내부 구현을 전부 검증하면 안된다.ex) service에서 어떤 reposiotry의 메소드가 호출되었는지 전부 verify 하기
이유:1. 내부 구현은 언제든 수정될수 있는데, 수정할 때마다 테스트코드를 수정해야 하고, 의미도 없음
https://jojoldu.tistory.com/614
참고:
https://velog.io/@lxxjn0/Test-Double%EC%9D%84-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90
http://sakula99.egloos.com/2912503
https://reflectoring.io/clean-unit-tests-with-mockito/
JUnit
JUnit
자바 코드를 unit-testing 하는 프레임워크.
JUnit5
자바8 이상부터 지원하고, 여러 테스팅 스타일을 가능하게 해줌
특히 자바8의 람다 표현식을 잘 지원함
3가지 서브 프로젝트로 나뉜다.
1. JUnit Platform
JUnit 프레임워크의 핵심이다.
JVM 위에서 테스팅 프레임워크를 실행하는 책임이 있다.
빌드 툴 등의 클라이언트와 JUnit 사이의 인터페이스를 제공한다. 그래서 junit이 테스트를 발견하고 실행할수 있게 해준다.
TestEngine Api라는 것도 제공하는데, JUnit 기반의 커스텀 테스팅 프레임워크를 개발해서 주입해 사용할수 있다고 한다.
2. JUnit Jupiter
JUnit5의 기능을 제공한다.
JUnit4에 없던 추가된 어노테이션 목록:
- @TestFactory – denotes a method that's a test factory for dynamic tests
- @DisplayName – defines a custom display name for a test class or a test method
- @Nested – denotes that the annotated class is a nested, non-static test class
- @Tag – declares tags for filtering tests
- @ExtendWith – registers custom extensions
- @BeforeEach – denotes that the annotated method will be executed before each test method (previously @Before)
- @AfterEach – denotes that the annotated method will be executed after each test method (previously @After)
- @BeforeAll – denotes that the annotated method will be executed before all test methods in the current class (previously @BeforeClass)
- @AfterAll – denotes that the annotated method will be executed after all test methods in the current class (previously @AfterClass)
- @Disable – disables a test class or method (previously @Ignore)
3. JUnit Vintage
JUnit3,4 기반의 테스트를 실행하게 해준다
기본 어노테이션
@BeforeAll //JUnit4에서는 Before. 동작은 같은데 이름이 바뀜
static void setup() {
//모든 테스트 메소드 실행 전 한번만 실행. static 이여야만 컴파일됨
}
@BeforeEach // JUnit4에서는 BeforeClass. 동작은 같은데 이름이 바뀜
void init() {
//각 테스트 전마다 실행
}
@AfterAll
static void done() {
//모든 테스트 메소드 실행 후 한번만 실행. static 이여야만 컴파일됨
}
@AfterEach
void tearDown() {
//각 테스트 후마다 실행
}
@DisplayName("1 + 1은 2여야만 한다.")
@Test
void testSingleSuccessTest() {
//테스트에 이름 붙이고 표시
}
@Disabled("Not implemented yet")
@Test
void testShowSomething() {
//이거 붙으면 실행 안함
}
참고: https://www.baeldung.com/junit
Assertion
Assertions
org.junit.jupiter.api.Assertions에 있음
JUnit 말고 AssertJ를 사용
@Test
void lambdaExpressions() {
List<Integer> numbers = Arrays.asList(1, 2, 3);
assertTrue(numbers.stream()
.mapToInt(i -> i)//람다 사용가능
.sum() > 50, () -> "Sum should be greater than 5");//실패하면 이 매세지 표시
}
@Test
void groupAssertions() {
int[] numbers = {0, 1, 2, 3, 4};
assertAll("numbers",
() -> assertEquals(numbers[0], 1),
() -> assertEquals(numbers[3], 3),
() -> assertEquals(numbers[4], 1)//실패시 MultipleFailuresError
);
}
Assumptions
테스트 하고자 하는것과 직접적인 관계 없지만,
테스트에 필요한 외적인 조건을 명시할수있다.
assumeTrue(), assumeFalse(), and assumingThat(): 등 사용함
@Test
void falseAssumption() {
assumeFalse(5 < 1);
assertEquals(5 + 2, 7);
}
예외 테스트
@Test
void shouldThrowException() {
Throwable exception = assertThrows(UnsupportedOperationException.class, () -> {
throw new UnsupportedOperationException("Not supported");
});
assertEquals(exception.getMessage(), "Not supported");
}
@Test
void assertThrowsException() {
String str = null;
assertThrows(IllegalArgumentException.class, () -> {
Integer.valueOf(str);
});
}
//이하 JUnit4
@Test(expected = NullPointerException.class)//단순히 어떤 에러 발생인지는 간단함
public void whenExceptionThrown_thenExpectationSatisfied() {
String test = null;
test.length();
}
@Rule//에러와 관련된 다른 테스트는 복잡함
public ExpectedException exceptionRule = ExpectedException.none();
@Test
public void whenExceptionThrown_thenRuleIsApplied() {
exceptionRule.expect(NumberFormatException.class);
exceptionRule.expectMessage("For input string");
Integer.parseInt("1a");
}
//JUnit5에서 이렇게 가능하지만, 가독성이 안좋음. assertj라면?
Dynamic Test
@TestFactory
public Stream<DynamicTest> translateDynamicTestsFromStream() {
return in.stream()
.map(word ->
DynamicTest.dynamicTest("Test translate " + word, () -> {
int id = in.indexOf(word);
assertEquals(out.get(id), translate(word));//이렇게 컴파일 타임에 생성되는 값으로 테스팅 가능
})
);
}
@RunWith
JUnit5에서는 @ExtendWith을 사용한다. 하위호환을 위해 사용가능하긴 하다
@RunWith(JUnitPlatform.class)//JUnit4 기반으로 테스트하겠다는걸 의미
public class GreetingsTest {
// ...
}
//JUnit5 기반으로 테스트하겠다는걸 의미
public class GreetingsTest {
// ...
}
@ExtendWith(SpringExtension.class)
//Spring 5 에서는 SpringExtension을 제공하여 JUnit5와 Spring TestContext를 결합한다고 한다.
@ContextConfiguration(classes = { SpringTestConfiguration.class })
public class GreetingsSpringTest {
// ...
}
JUnit assert vs Mockito verify
The Assert command is used to validate critical functionality. If this validation fails, then the execution of that test method is stopped and marked as failed.
In the case of Verify command, the test method continues the execution even after the failure of an assertion statement. The test method will be marked as failed but the execution of remaining statements of the test method is executed normally.
JUnit은 실패하면 멈추고, Mockito는 실패해도 나머지는 실행됨
AssertJ + 다른것과 비교
AssertJ
JUnit의 기본 assertion을 사용하지 않고 AssertJ를 사용하는게 여러모로 좋다.
AssertJ의 장점 (vs JUnit)
1. 메소드 체이닝이 가능하다.
- 가독성이 좋다
- 자연어같이 읽힌다
2. 파라미터 순서가 안햇갈린다
- assertEquals(expected, actual) vs assertThat(actual).isEqualTo(expected)
- 후자가 더 직관적인것같다
3. 외울게 적다 (자동완성이 잘된다)
- JUnit의 assertTrue(), assertEquals() 등 보다 assertThat().is~~ 이게 더 편리하다
- 매 줄마다 assertTrue(), assertEquals() 적는거보다 메소드 체이닝으로 .is 까지 적어주면 IDE가 자동완성을 잘 해줘서 편했다.
vs hamcrest
아래는 hamcrest를 사용한 테스트이다
//hamcrest code
@Test
public void test_allOf() {
String str = "MyTest";
assertThat(str, allOf(is("MyTest"),
startsWith("My"),
containsString("Test")));
}
1. allOf()이라는 메소드를 미리 외우고 있어야하고,
2. 괄호가 많이 들어가서
불편하고 가독성이 안좋다.
메소드 체이닝 방식이 주는 편리함이 큰듯하다.
JUnit vs Spock
JUnit + assertJ/Hamcrest + Mockito 가 아닌
Spock이라는 프레임워크를 사용할수도 있다.
참고 : https://meetup.toast.com/posts/268
import spock.lang.*
//spock 이용 테스트
class MySpockTest extends Specification {
def "더하기 테스트"() {
given:
def x = 10;
def y = 20;
when: // stimulus
def sum = x + y
then: // response
sum == 30
}
}
given, when, then 으로 구성해서 BDD를 사용하고, 코드도 직관적이여 보인다.
위의 링크에서 확인할수 있듯이, mocking 또한 간단하고(훨씬), 코드 중복도 많이 줄어든다.
참 좋아보인다.
그런데 이건 사용 언어가 groovy라서...
일단 나는 JUnit을 사용하는게 좋은것같다.
사용 예시
assertThat(frodo)
.isNotEqualTo(sauron)
.isIn(fellowshipOfTheRing);
assertThat(frodo.getName())
.startsWith("Fro")
.endsWith("do")
.isEqualToIgnoringCase("frodo");
assertThat(fellowshipOfTheRing)
.hasSize(9)
.contains(frodo, sam)
.doesNotContain(sauron);
1. Object Assertions
Dog fido = new Dog("Fido", 5.25);
Dog fidosClone = new Dog("Fido", 5.25);
assertThat(fido).isEqualTo(fidosClone);//실패. 참조 비교하기 때문
assertThat(fido).isEqualToComparingFieldByFieldRecursively(fidosClone);// 성공
2. Boolean Assertions
assertThat("".isEmpty()).isTrue();
3. Iterable / Array Assertions
List<String> list = Arrays.asList("1", "2", "3");
assertThat(list).contains("1");
assertThat(list).isNotEmpty();
assertThat(list).startsWith("1");
assertThat(list)
.isNotEmpty()
.contains("1")
.doesNotContainNull()
.containsSequence("2", "3");//체이닝 가능
@Test
void whenTestingForOrderAgnosticEqualityBothList_ShouldNotBeEqual() {
List a = Arrays.asList("a", "a", "b", "c");
List b = Arrays.asList("a", "b", "c");
assertThat(a).hasSameElementsAs(b);
}
4. Exception Assertions
assertThatThrownBy(() -> bookInfoService.createBookInfo(noIsbn))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("isbn 정보가 없습니다.");
Mockito + 다른것과 비교
Mockito
mockito annotations
@RunWith(MockitoJUnitRunner.class)//이래야 사용 가능. JUnit4
public class MockitoAnnotationTest {
...
}
@ExtendWith(MockitoExtension.class)//이래야 사용 가능. JUnit5
public class MockitoAnnotationTest {
...
}
@InjectMocks
private BookInfoService bookInfoService;//테스트 대상인 클래스
@Mock
private BookInfoRepository mockedBookInfoRepository;//mocking 할 클래스
private static BookInfo bookInfo;
@BeforeAll
static void setup(){
Long id = 1L;
String name = "bookInfo_1";
String isbn = "isbn_1";
bookInfo = BookInfo.builder()
.id(id)
.name(name)
.isbn(isbn)
.build();
}
List<String> mockedList = mock(MyList.class);
mockedList.size();
verify(mockedList, times(1)).size();
Mockito.mock() vs @Mock vs @MockBean
//Mockito.mock() 사용
@Test
public void givenCountMethodMocked_WhenCountInvoked_ThenMockedValueReturned() {
UserRepository localMockRepository = Mockito.mock(UserRepository.class);
Mockito.when(localMockRepository.count()).thenReturn(111L);
long userCount = localMockRepository.count();
Assert.assertEquals(111L, userCount);
Mockito.verify(localMockRepository).count();
}
//@Mock 사용
//이걸 사용하자.
//사용이 더 쉽고, 테스트 실패시 메세지에 어느 필드에서 문제가 생겼는지 알려준다.
@RunWith(MockitoJUnitRunner.class)//@Mock 사용시 이렇게 명시해줘야 사용 가능
public class MockAnnotationUnitTest {
@Mock
UserRepository mockRepository;
@Test
public void givenCountMethodMocked_WhenCountInvoked_ThenMockValueReturned() {
Mockito.when(mockRepository.count()).thenReturn(123L);
long userCount = mockRepository.count();
Assert.assertEquals(123L, userCount);
Mockito.verify(mockRepository).count();
}
}
//@MockBean 사용
//spring application context에 mock bean을 주입해준다. 원래없던거면 새로 만든다.
//Integration 테스트시 유용하다 - 외부 서비스를 모킹하면 좋다 ex)결제
@RunWith(SpringRunner.class)
public class MockBeanAnnotationIntegrationTest {
@MockBean
UserRepository mockRepository;
@Autowired
ApplicationContext context;
@Test
public void givenCountMethodMocked_WhenCountInvoked_ThenMockValueReturned() {
Mockito.when(mockRepository.count()).thenReturn(123L);
UserRepository userRepoFromContext = context.getBean(UserRepository.class);
long userCount = userRepoFromContext.count();
Assert.assertEquals(123L, userCount);
Mockito.verify(mockRepository).count();
}
}
Mockito와 다른것 비교
vs JMock, EasyMock
- 이때 Mockito가 가장 좋은 이유는 Mockito가 마틴파울러가 이야기한 Mocks aren't Stubs의 원칙을 지키는 프레임워크이기 때문이라고 한다. 위에 써놓은 내용임.
vs PowerMock
- Mockito or EasyMock을 확장하여 더 다양한 기능을 사용할수 있다고 한다.
- PowerMock은 unit testing 에 대해 전문적인 지식을 가진 사람을 위한 것이고, 익숙하지 않는 개발자에게는 좋은 점보다 나쁜 점이 더 많을 수 있다고 한다.
- 객체지향 원칙을 지키지 않아도 되도록 하는 측면이 있기때문이라고 함.
참고 : https://wonit.tistory.com/493
https://martinfowler.com/articles/mocksArentStubs.html
BDD
BDD
Behavior Driven Development
테스트 코드의 가독성을 높여줄 목적으로 탄생한 개념이다.
자연어와 비슷하게 행동 중심으로 테스트 코드를 작성하여,
가독성 & 유지보수성을 높일수 있다.
각 테스트를 given, when, then으로 구성하여 작성한다.
given - 테스트의 조건
when - 어떤 동작을 하는 부분(테스트 할 부분)
then - 결과를 검증하는 부분(assertion)
@Test
void plus() {
// given
int a = 10;
int b = 20;
// when
int result = calc.plus(a, b);
// then
assetThat(result).isEqualTo(a + b);
}
BDDMockito
BDD 방법론을 잘 지키기 위해, Mockito에서 지원하는 API이다.
import static org.mockito.BDDMockito.*;
BDDMockito를 이용하면 Mockito를 BDD에 맞게 사용가능.
given, when, then 으로 잘 구분 가능해짐.
given(phoneBookRepository.contains(momContactName))
.willReturn(false);
TDD
TDD
Test Driven Development
보통의 테스트 코드 작성 순서:
기능개발 -> 테스트코드 작성
TDD:
테스트 작성 -> 기능개발
1. 반드시 실패하는 테스트 작성
2. 테스트를 통과시키기(기능 개발)
3. 리펙토링
장점 :
1. 빠른 피드백
- 기능을 구현하고 나서 테스트를 실행하는 것만으로 제대로 코딩했는지 알수있기 때문
2. 과거 의사결정 상기
- 테스트 코드를 보며 미리 생각해놨던 의사결정을 쉽게 알수있다.
- 마치 슈도 코드를 보는 느낌
단점 :
1. 초기 학습비용 :
- 익숙하지 않은 방법이라서 학습하고 실천하기 위한 비용이 발생함
2. 시간이 더 오래걸릴 수 있음:
- 미리 테스트 코드를 작성하고, 중간중간 고쳐야 하기때문. 10% ~ 30% 정도 시간이 더 걸릴수 있다고 함
나는 일단 테스트코드를 나중에 짜는거에 익숙해 지고 나서 적용해보는게 더 좋을것같다.
'Dev > Spring' 카테고리의 다른 글
자바 스프링 예외처리 - unchecked, checked 예외 (0) | 2022.06.20 |
---|---|
테스트 작성과 Jacoco (0) | 2022.04.13 |
Controller와 Service의 역할에 대한 고민 (2) | 2021.12.17 |
[Lombok ] 클래스 단위로 @Builder 사용시 주의점 (0) | 2021.08.20 |
Servlet 서블릿에 대하여 (0) | 2021.07.17 |