자바에서 mockito를 이용해서 단위 테스트에 있어 복잡한 참조 관계를 잘 떨어뜨려서 우리가 원하는 목적에 맞는 테스트를 작성할 수 있었습니다. 예를 들면 아래 같은 녀석이 그렇죠.

@RunWith(MockitoJunitRunner.class)
public class DeveloperTest {
    @Mock
    ProductManager mockPM;

    @Mock
    Designer mockDesigner;

    Developer subject = new Developer();

    @Before
    public void setUp() throws Exception {
        subject.workWith(mockPM);
        subject.workWith(mockDesigner);
    }

    @Test
    public void testWriteWebPage() throws Exception {
        when(mockPM.writeAcceptanceCriteria(anyString())).thenReturn("Nice AC");
        when(mockDesigner.drawPage(anyString())).thenReturn(mock(Drawing.class));
        
        String page = subject.writeWebPage("story-1");

        assertThat(page)
            .startWith("<!doctype html>")
            .endWith("</html>");
        verify(mockPM).writeAcceptanceCriteria(eq("story-1"));
        verify(mockDesigner).drawPage(eq("story-1"));
    }
}

실제 우리가 일하는 방식과 차이는 있습니다만, 일단 개발자가 일하기 위해선 주변 사람들이 도와준다는 가정입니다. Developer 클래스는 ProductManager 클래스와 Designer 클래스에 의존하는데 Developer 클래스의 writeWebPage() 테스트할 때 의존 관계에 있는 클래스까지 참조하기 때문에 테스트가 실패하면 어디서 발생하는 지 모르는 문제점이 발생하게 됩니다.(부수 효과) 이를 위해서는 관심을 갖는(대상) 범위 외에 나머지를 제외시켜야할텐데 이 때 mocking을 쓰면 매우 적절하죠. 그럼 파이썬에서는 어떻게 할 수 있을까요?

unittest fundamentals

파이썬에 다양한 단위 테스트 프레임워크가 있지만 가장 기본인 unittest를 기준으로 하면 다음과 같이 간단하게 작성할 수 있습니다.

from unittest import TestCase

import developer as subject

class DeveloperTest(TestCase):
    def test_write_web_page(self):
        page = subject.write_web_page('story-1')
        
        self.assertTrue(page.startswith('<!doctype html>'))
        self.assertTrue(page.endswith('</html>'))

TestCase 내에 assertTrue 외에도 다양한 함수가 있으니 여기를 참고해서 작성하면 됩니다.

single mock

mock object를 한 개만 둘 때입니다.

from unittest import mock, TestCase

class DeveloperTest(TestCase):
    @mock.patch('ProductManager.writeAcceptanceCriteria')
    def test_write_web_page(self, mock_ac):
        mock_ac.return_value = 'a sophisticated ac'

        page = subject.write_web_page('story-1')
        
        self.assertTrue(page.startswith('&lt;!doctype html&gt;'))
        self.assertTrue(page.endswith('&lt;/html&gt;'))

        mock_ac.assert_called_with('a sophisticated ac')
  1. 해당 테스트 케이스 함수에서 @mock.patch 데코레이터를 선언하여 mocking 대상이 되는 함수를 선언하고,
  2. 테스트 케이스 함수 파라미터를 지정합니다.
  3. mocking 함수에 대해 결과값을 mock_object.return_value = expected 형태로 선언하고,
  4. 대상 함수를 테스트 한 후 verify하는 시점에 mock_object.assert_called_with(expected) 형태로 검증합니다.

matcher 쓰기

자바 진영의 mockito에서 anyString()any(classType)와 같이 매칭을 지정할 수도 있는데, unittest에도 unittest.mock.ANY로 사용할 수 있습니다.

# second_param can be anything
mock_object.assert_called_with('first_param', ANY, 'third_param')

multiple mocks

mocking 대상이 두 개 이상일 때도 큰 차이는 없습니다만, 아래와 같이 데코레이터로 선언한 순서와 파라미터 순서가 반대인 것만 주의하면 됩니다.

from unittest import mock, TestCase
import Drawing

class DeveloperTest(TestCase):
    @mock.patch('ProductManager.writeAcceptanceCriteria')
    @mock.patch('Designer.drawPage')
    def test_write_web_page(self, mock_draw, mock_ac):
        mock_ac.return_value = 'a sophisticated ac'
        mock_draw.return_value = 'fake drawing'

        page = subject.write_web_page('story-1')
        
        self.assertTrue(page.startswith('&lt;!doctype html&gt;'))
        self.assertTrue(page.endswith('&lt;/html&gt;'))

        mock_ac.assert_called_with('a sophisticated ac')
        mock_draw.assert_called_with('fake drawing')

multiple calls

하나의 함수에 대해 다양한 인자로 여러번 호출할 때에는 assert_called_with 대신 assert_has_calls를 사용해야 합니다.

from unittest.mock import call

mock_ac.assert_has_calls([call('a sophisticated ac'), call('second ac')])

한계점

  • assert_has_only_calls 와 같은 함수가 있다면, 해당 함수를 호출하는 인자를 정확하게 한정할 수 있을 것 같은데, 현재는 call count를 세고 assert_has_calls로 검사하는 식으로 대안을 찾을 수 있습니다.

Reference