Mocking in python unittest
자바에서 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('<!doctype html>'))
self.assertTrue(page.endswith('</html>'))
mock_ac.assert_called_with('a sophisticated ac')
- 해당 테스트 케이스 함수에서
@mock.patch
데코레이터를 선언하여 mocking 대상이 되는 함수를 선언하고, - 테스트 케이스 함수 파라미터를 지정합니다.
- mocking 함수에 대해 결과값을
mock_object.return_value = expected
형태로 선언하고, - 대상 함수를 테스트 한 후 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('<!doctype html>'))
self.assertTrue(page.endswith('</html>'))
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
로 검사하는 식으로 대안을 찾을 수 있습니다.