OOP(Object oriented Programming)에서는 무엇을 테스트 해야할까?

colin.jang
13 min readDec 15, 2020

--

출처: https://pixabay.com/photos/code-code-editor-coding-computer-1839406/

처음 개발을 할 때는 기능 수정을 위해 코드를 변경하는 것이 크게 어렵지 않았다.

하지만 점점 큰 시스템에서 코드를 수정할 때마다 간단한 코드 변경이 어떤 사이드이펙트가 발생할지 몰라 조금씩 조금씩 코드를 변경하는 게 어려워만 갔고 두려웠다.

간단한 기능을 추가하는 것조차 부담이 되곤 했었다.

그러던 중 테스트를 작성해야 된다는 것을 알게 되었고 지금도 많은 사람들이 테스트의 중요성에 대해 말하곤 한다.

그렇지만 막상 처음 테스트를 작성하려고 하니 무엇을 테스트를 해야 하는 건지 아예 감조차 잡히지 않았다.

이 글은 그동안 테스트 작성을 위해 고민했던 경험과 방법을 공유하고자 한다.

통합 테스트, 인수 테스트, 단위 테스트 등 많은 테스트 코드 작성 방법이 있겠지만 글에서는 mock을 활용한 단위 테스트만을 다뤘다.

글에서 나오는 코드들은 Java, Junit5, Mockito를 사용했다.

제목처럼 OOP에서는 무엇을 테스트 해야할까?

먼저 객체지향프로그래밍에서 객체를 지향한다는 건 무엇일까?

객체지향 프로그래밍이란 말 그대로 객체를 지향하는 프로그래밍.

객체지향 설계에서 가장 중요한 일은 올바른 책임을 올바른 객체에게 할당하고
객체간의 유연한 협력관계를 구축하는 일이다.

- 오브젝트 조영호.

따라서 객체지향프로그래밍에서 테스트란 해당 객체가 올바른 책임을 가지고 협력에 참여하는 지를 테스트해야한다.

간단히 요일별 알람시간 정보를 저장하는 프로그램이 있다고 가정해보자.

요일별 알람시간을 저장하는 Entity

알람시간 정보가 해당요일에 있다면 해당 요일에 알람을 활성화한 다음 시간을 수정하고 알람시간 정보가 없다면 새로 알람시간을 저장해야한다고 해보자.

요일별 알람시간을 저장하는 Service. (validation은 생략…)

여기서 AlarmDayService의 역할과 책임, 그리고 협력은 무엇일까?

AlarmDayService에서는 saveAlarmDay()라는 public interface를 통해 메세지를 전달 받고 해당 메세지를 AlarmDayRepository 객체의 public interface인 save() 메소드에 메세지를 전달하고 있다.

테스트를 작성해보자.

@DisplayName("알람 저장 테스트")
@Test
void test() {
// given
AlarmDayRequest request = mock(AlarmDayRequest.class);
given(request.getUserId()).willReturn(USER_ID);
given(request.getItems()).willReturn(
List
.of(
new AlarmDayRequest.AlarmDayItem(
DayOfWeek
.MONDAY,
LocalTime.of(7, 0, 0)
)
,
new AlarmDayRequest.AlarmDayItem(
DayOfWeek
.TUESDAY,
LocalTime.of(7, 10, 0)
)
,
new AlarmDayRequest.AlarmDayItem(
DayOfWeek
.WEDNESDAY,
LocalTime.of(7, 20, 0)
)
)
)
;

AlarmDay monday = mock(AlarmDay.class);
AlarmDay tuesday = mock(AlarmDay.class);
AlarmDay thursday = mock(AlarmDay.class);
AlarmDay friday = mock(AlarmDay.class);
given(monday.getDayOfWeek()).willReturn(DayOfWeek.MONDAY);
given(tuesday.getDayOfWeek()).willReturn(DayOfWeek.TUESDAY);
given(thursday.getDayOfWeek()).willReturn(DayOfWeek.THURSDAY);
given(friday.getDayOfWeek()).willReturn(DayOfWeek.FRIDAY);
given(alarmDayRepository.findAllByUserId(USER_ID))
.willReturn(
Set
.of(
monday,
tuesday,
thursday,
friday
)
)
;

// when
alarmDayService.saveAlarmDay(request);

// then
??????????????????????

코드의 흐름을 간략히 설명하면 유저는 기존의 월, 화, 목, 금요일에 이미 알람시간을 가지고 있다.
request에는 월요일, 화요일, 수요일에 대한 알람시간을 저장하기 위한 정보를 가지고 있다.

그렇다면 saveAlaramDay() 메소드는 해당 request를 통해 기존에 저장되어있던
월요일, 화요일의 알람시간을 7시, 7시10분으로 변경하고
해당 알람들을 활성화 할 것이고
수요일의 알람시간은 새로 저장할 것이다.
목요일과 금요일에 알람시간은 비활성화 할 것이다.

여기서 saveAlarmDay() 메소드의 경우 리턴 타입이 void이다.
then에서는 과연 무엇을 검증해야할까?

public void saveAlarmDay(AlarmDayRequest request); // 기존public List<AlarmDay> saveAlarmDay(AlarmDayRequest request); // 변경// when
List<AlarmDay> result = alarmDayService.saveAlarmDay(request);

// then
assertEquals(5, result.size());
assertEquals(DayOfWeek.MONDAY, result.get(0).getDayOfWeek());
...

처음에는 메소드 검증을 위해서 saveAlarmDay() 메소드의 시그니처를 변경하여 void -> List<AlarmDay>로 변경한 후에 then에서는 해당 리스트에 대한 검증을 진행한 적도 있었다.

위와 같은 코드로 과연 객체망을 검증했다고 할 수 있을까?

위에서도 얘기한것처럼 객체지향 프로그래밍에서는
객체의 역할과 책임, 객체간의 협력관계를 구축하는 것이다.

saveAlarmDay() 메소드의 리턴값을 통해 검증을 진행한다고 한 들 내부에 있는 private 메소드인 insertOrUpdateAlarmDay()에 대한 검증은 이루어진걸까?

더군다나 외부 클라이언트 관점에서 AlarmDayService 객체의 public interface인 saveAlarmDay() 메소드 이외에는 캡슐화 되어있기 때문에 해당 객체가 내부에서 AlarmDayRepository 객체와 협력하고 있는지는 알 수 없다. insertOrUpdateAlarmDay() 메소드 내부에는 위에서 강조한 다른 객체와의 협력을 진행하고 있다.(AlarmDayRepository 객체의 public interface인 save() 메소드에게 메세지를 전달하고 있다.)
어떻게 해야 올바른 객체망을 검증할 수 있을까?

Mock 객체의 목적은 무엇일까? 단순히 외부 서비스에 대한 응답을 전달하는 용도로만 사용하고 있다면 그걸로는 부족하다.
Mock 객체의 진정한 목적은 올바른 객체망을 검증하는데 있다.

객체망이 올바르게 검증되지 않았다면 상태를 가지고 있는 객체지향프로그래밍에서 찾기 힘든 오류가 발생할 수 있다.

비행기 슈팅 게임에서 2번째 보스에서 1번 죽고나서 필살기 아이템을 먹은 후 다시 한 번 죽은 다음에 필살기를 쓰려고 하면 오류가 발생하는 경우

(물론 예시를 위해 과장된 표현입니다…)

mocking된 객체의 검증을 위해 Mockito 라이브러리에서는 verify()라는 메소드를 제공한다.

코드에서는 해당 유저가 이미 가지고 있는 알람정보를 mocking 하였다.(월, 화, 목, 금의 알람정보)

AlarmDay monday = mock(AlarmDay.class);
AlarmDay tuesday = mock(AlarmDay.class);
AlarmDay thursday = mock(AlarmDay.class);
AlarmDay friday = mock(AlarmDay.class);

verify()메소드의
첫번째 인자로는 mock 객체
두번째 인자로는 VerificationMode 객체
가 사용된다.

// VerificationMode 종류
verify(mock, times(5)).someMethod("was called five times");
verify(mock, never()).someMethod("was never called");
verify(mock, atLeastOnce()).someMethod("was called at least once");
verify(mock, atLeast(2)).someMethod("was called at least twice");
verify(mock, atMost(3)).someMethod("was called at most 3 times");
verify(mock, atLeast(0)).someMethod("was called any number of times"); // useful with captors
verify(mock, only()).someMethod("no other method has been called on the mock");

verify()메소드를 활용해 위 객체들을 검증해보자.

  • 월요일, 화요일의 알람시간을 7시, 7시10분으로 변경하고 해당 알람들을 활성화 할 것
  • 수요일의 알람시간은 새로 저장할 것
  • 목요일과 금요일에 알람시간은 비활성화 할 것
// then
assertAll(
// monday
()
-> verify(monday, times(1))
.setAlarmTime(LocalTime.of(7, 0, 0)),
() -> verify(monday, times(1))
.setIsEnabled(true),
// tuesday
()
-> verify(tuesday, times(1))
.setAlarmTime(LocalTime.of(7, 10, 0)),
() -> verify(tuesday, times(1))
.setIsEnabled(true),
// thursday
()
-> verify(thursday, times(1)).setIsEnabled(false),
// friday
()
-> verify(friday, times(1)).setIsEnabled(false)
)
;

수요일은 기존에는 없던 요일이기 때문에 새로운 알람정보를 저장하기 위해 AlarmDayRepository 객체와 협력을 진행한다.
따라서 AlarmDayRepository에 전달된 메세지에 대한 검증이 필요하다.
위와 동일한 방법으로 진행해보자.

verify(alarmDayRepository, times(1))
.save(
AlarmDay.builder()
.userId(USER_ID)
.dayOfWeek(DayOfWeek.WEDNESDAY)
.alarmTime(LocalTime.of(7, 20, 0))
.build()
)
;
테스트는 실패한다.

테스트가 실패하는 이유는 무엇일까?
AlarmDayRepository 객체에 save() 메소드에 전달되는
AlarmDay 객체가 다르기 때문이다.

// 테스트코드에서 생성된 객체
verify(alarmDayRepository, times(1))
.save(
AlarmDay.builder()
.userId(USER_ID)
.dayOfWeek(DayOfWeek.WEDNESDAY)
.alarmTime(LocalTime.of(7, 20, 0))
.build()
)
;
// 실제 서비스에서 생성된 객체
alarmDayRepository.save(
AlarmDay.builder()
.userId(request.getUserId())
.alarmTime(alarmDayItem.getAlarmTime())
.dayOfWeek(alarmDayItem.getDayOfWeek())
.isEnabled(true)
.build()
)
;

두 객체는 당연히 다른 인스턴스이기 때문에 테스트는 실패한다.
Mockito에서는 ArgumentMatcher를 제공한다.

verify(alarmDayRepository, times(1))
.save(any(AlarmDay.class));

any()를 통해 save 메소드에 전달되는 클래스 타입을 지정해줄 수 있다.

테스트 통과!

그렇지만 위와 같은 방법으로는 save 메소드에 전달되는 AlarmDay 메세지가 과연
수요일인지
시간은 7시 20분인지
활성화는 되어있는건지
검증할 수 없다.
Mockito에서는 이를 위해 ArgumentCaptor를 제공한다.

간단히 말해 메소드가 실행되는 시점에 전달되는 메세지를 캡처할 수 있다.

var dayCaptor = ArgumentCaptor.forClass(AlarmDay.class);
verify(alarmDayRepository, times(1)).save(dayCaptor.capture());

위 테스트를 통해 alarmDayRepository 객체에 save() 메소드가 한 번만 호출되었고

// captor의 getValue() 메소드를 통해 캡처된 메세지를 얻을 수 있다.
AlarmDay value = dayCaptor.getValue();
assertEquals(DayOfWeek.WEDNESDAY, value.getDayOfWeek());
assertEquals(LocalTime.of(7, 20, 0), value.getAlarmTime());
assertTrue(value.getIsEnabled());

위 테스트를 통해 AlarmDayRepository 객체에 전달된 메세지가 무엇인지 검증 할 수 있다.

AlarmDayRepository에 전달된 메세지는
수요일이며
알람시간은 7시 20분
활성화 되어있음 을 검증할 수 있다.

처음 블로그 글을 써보았는데 글을 쓴다는게 엄청 어려울거라 생각했는데 직접 써보니까 더 어려운거 같다… 글 마무리를 어떻게 해야하는건지 원…

객체지향프로그래밍에 대해 더 공부하고 싶으신 분들이 있으시다면
객체지향 관련 정말 좋은 강의가 있어서 추천드립니다.

잘못된 정보에 대해서 피드백은 감사히 받겠습니다.

--

--

colin.jang
colin.jang

No responses yet