Mock에 대해(Test double 포함)

Mock

Mock 객체란 모듈의 겉모양이 실제 모듈과 비슷하게 보이도록 만든 가짜 객체.

모듈이 필요로 하는 의존성이 테스트 케이스 작성을 어렵게 만드는데, 이 의존성을 단절하기 위해 Mock 객체가 사용됨.

언제 Mock 객체를 만들 것인가?

  1. 테스트 작성을 위한 환경 구축이 어려울 때
    • 환경 구축을 위한 작업 시간이 많이 필요한 경우 Mock 객체를 사용
    • 특정 모듈을 아직 갖고 있지 않아서 테스트 환경을 구축하지 못할 경우
    • 타 부서와의 협의나 정책이 필요한 경우에도 Mock이 필요.
      • 연계 모듈이라서 다른쪽에서 승인을해줘야 테스트가 가능한 경우, 방화벽으로 막혀 있어서 통과가 어려운 경우등이 이에 속함.
  2. 테스트가 특정 경우나 순간에 의존적일 때
  3. 테스트 시간이 오래 걸리는 경우

테스트 더블

오리지널 객체로 테스트를 진행하기 어려울 경우 이를 대신해서 테스트를 진행할 수 있도록 만들어주는 객체.(Mock과 의미가 조금 겹침.)

  • Test Double
    • Dummy Object
      • 단순한 껍데기에 해당.
      • 단지 인스턴스화된 객체가 필요할 뿐 해당 객체의 기능까지는 필요하지 않은 경우에 사용.
      • 따라서 해당 더미 객체의 메소드가 호출됐을 때의 정상 동작은 보장되지 않음.
    • Test Stub
      • 더미 객체가 마치 실제로 동작하는 것처럼 보이게 만들어놓은 객체.
      • 객체의 특정 상태를 가정해서 만들어놓은 단순 구현체.
      • 인스턴스화된 객체가 특정 상태나 모습을 대표하면 스텁임.
    • Fake Object
      • 여러 개의 인스턴스를 대표할 수 있는 경우
      • 좀 더 복잡한 구현이 들어가 있는 객체
      • 보통 List나 Map을 이용해서 DB같은 외부 의존 환경을 대체함.
      • 복잡한 로직이나, 객체 내부에서 필요로하는 외부 객체들의 동작을 비교적 단순화하여 구현한 객체.
        • 결과적으로 테스트 케이스 작성을 진행하기 위해 필요한 다른 객체(혹은 클래스)들과의 의존성을제거하기 위해 사용.
    • Test Spy
      • 테스트에 사용되는 객체에 대해서 특정 객체가 사용됐는지, 그리고 그 객체의 예상된 메소드가 정상적으로 호출됏는지를 확인해야 하는 상황이 발생함.
      • 이런 호출 여부를 몰래 감시해서 기록했다가 나중에 요청이 들어오면 해당 기록 정보를 전달해줌.
    • Mock Object

상태 기반 테스트 vs 행위 기반 테스트(Mock 객체를 이해하려면 상태 기반 테스트와 행위 기반 테스트에 대한 이해가 필요함)

  • 상태 기반 테스트(state base test)
    • 특정한 메소드를 거친 후, 객체의 상태에 대해 예상값과 비교하는 방식이 상태 기반 테스트.
      • eg. setName() 메소드를 호출했으면, getName() 메소드로 확인해보는 식
  • 행위 기반 테스트(behavior base test)
    • 올바른 로직 수행에 대한 판단의 근거로 특정한 동작의 수행 여부를 이용한다.
    • 메소드의 리턴값이 없거나 리턴값을 확인하는 것만으로는 예상대로 동작했음을 보증하기 어려운 경우에 사용.
    • eg. methodA에 A가 입력되면 methodB는 호출되지 않아야 정상이다. 그리고 B가 입력되면 methodB가 호출돼야 정상이다. 하지만 테스트 대상이 되는 methodA만 놓고 봤을 때 입력값에 대한 차이를 methodA만으로는 알아낼 수 없다. 즉 methodB의 호출 여부를 조사하지 않으면 methodA의 정상 동작 여부를 판단할 수 없다. 만일 methodA가 정상 동작햇을 경우 methodB가 반드시 호출되는 구성이라면, 반대로 methodB의 호출 여부로 methodA의 정상 여부를 판단할 수 있다고 보는 것임. 따라서 이럴 때는 methodB의 호출 여부를 확인하는 것이 테스트 시나리오의 종료조건이 됨.
      • 하지만 전통적인 테스트 케이스 작성 방식인 상태 기반 테스트에선 이런 상황에 대한 테스트 케이스를 작성하기가 매우 어렵거나 불편했음. 테스트 대상인 A가 상태를 갖고 있지 않기 때문이다.(void 와 같은..)
      • 이때 테스트 스파이 객체를 사용하거나 자체적으로 검증 기능을 제공하는 Mock 객체를 따로 만들어서 테스트 케이스를 작성하는 것임.
      • 행위를 점검하는 걸로 테스트 케이스를 만드는 방식..
      • 따라서 행위 기반 테스트를 수행할 때는 예상하는 행위들을 미리 시나리오로 만들어놓고 해당 시나리오대로 동작이 발생했는지 여부를 확인하는 것이 핵심이 된다.

Mock Object

  • 일반적인 테스트 더블은 상태(state를 기반으로 테스트 케이스를 작성함.
  • Mock 객체는 행위(behavior)를 기반으로 테스트 케이스를 작성함.

Mock 객체는 행위를 검증하기 위해 사용되는 객체를 지칭.

행위 기반 테스트는 좀 복잡한 시나리오가 사용되는 경우가 많고, 모양이나 작성 등 여러 측면에서 어색한 경우가 많아서 만약 상태 기반으로 테스트를 할 수 있는 상황이라면 굳이 행위 기반 테스트 케이스는 만들지 않는 것이 좋다.

어떤 mock object framework를 쓰는가가 중요한 게 아니라, 무엇을 mock object로 만들것인가에 대해서 고민하자.

Mockito

Mockito는 Stub 작성과 verify가중심을 이루며 다음과 같은 순서로 진행됨.

  1. CreateMock: 인터페이스에 해당하는 Mock 객체를 생성
  2. Stub: 테스트에 필요한 Mock 객체의 동작을 지정(필요시에만)
  3. Exercise: 테스트 메소드 내에서 Mock 객체를 사용
  4. Verify: 메소드가 예상대로 호출돘는지 검증
spy

실제 객체를 Mock으로도 만들 수 있다. 너무 강력해서 오히려 문제가 될 수도 있는 부분.
부분 Mocking이라는 방법으로 서드파티 제품, 고칠 수 없는 라이브러리만 남아있는 코드 등에 대해서만 한정적으로 사용하는 걸 권장.
Mockito의 저자는 spy 기능을 쓰게 되면 그건 이미 뭔가 잘못된 코드를 건드리고 있다는 증거라고 얘기함.

Mock 사용 시 유의사항
  • Mock 프레임워크가 정말 필요한지 잘 따져본다.
    • 기능과 방식에 매료되서 Mock 사용하는 것 자체가 목적이 돼버리는 경우가 종종 있는데,
    • 개발을 진행해나가면서 Mock 객체가 필요한 부분이 나오는 것이 아니라, Mock 객체가 적용될 수 있는 부분을 찾아내려고 애쓰는 역전현상이 발생함..
    • 한번 Mock 프레임워크를 사용하면 그뒤로는 해당 테스트 케이스를 유지하는 데 드는 비용이 지속적으로 발생함.
    • Mock 객체들은 깨지귀 쉬운 테스트 케이스가 돼버리는 경향이 있음.
    • 가능하다면 Mock이 필요 없는 의존성 적은 구조로 만들어 놓는 게 좋음.
  • ROI(투자 대비 수익)이 확실할 때만 사용
  • 어떤 Mock 프레임워크를 사용하느냐는 핵심적인 문제가 아니다.
  • Mock은 Mock일 뿐.
    • Mock 객체를 사용해 잘 작동하는 코드를 만들었다 해도 실제 객체가 들어왔을 때 잘 동작하리란 보장은 없다.
    • 언젠가는 Mock 객체를 실제 객체로 대체되어 테스트를 해야하는 시점이 온다.
      • 초반에 실제 객체를 테스트에 사용할 수 있고, 그 비용이 크지 않다면 Mock을 사용하지 않는 것을 추천.

Mockito 개발자가 이야기하는 궁극의 TDD 템플릿

BDD 스타일을 따르는 템플릿

@Test
public void shouldDoSomethingColl() throws Exception{
    //given : 선행조건 기술

    //when : 기능 수행

    //then : 결과 확인 
}

테스트 작성시 도움도 많이되고, 다른 개발자들이 보기에도 도움이 됨. 의미적으로 각 단계를 구분해주고 있음.