테스트가 필요하다는 것을 어디서 많이 주워들어서 알고는 있었다.
하지만 회사 레파지토리에서는 테스트 코드를 찾을 수 없었고, 나도 테스트 코드를 접해본적이 없어 테스트 코드를 작성할 생각을 하지 않고 개발을 계속했다. 그러다보니 테스트가 안된 코드가 커밋되는 일이 비일비재했고 큰 단위의 모듈을 테스트하며 오류가 발생하면 어디서 발생하는 오류인지도 찾기가 힘들었다.
무엇보다 리팩토링이나 기능추가 등 코드를 수정할때마다 데이터를 확인하며 테스트를 해야하는 반복작업이 생겼다. 물론 귀찮은 건 누구나 똑같기때문에 그 반복작업은 잘 이루어지지 않았다. 그래서 테스트 코드를 추가하기로 마음먹었다. 그런데 테스트 코드는 어떻게 작성해야 하는걸까? 먼저 테스트 코드에 대한 가이드라인을 찾아봤다.
🧐 각 기능의 가장 작은 단위에 집중하여 해당 기능이 정확히 동작하는지 증명한다.
🚩 각 테스트 유닛은 독립적이어야 한다. 순서나 같이 실행되는 유닛과 상관없어야 한다.
⏰ 테스트는 빠르게 돌아야 한다. 무거운 테스트는 따로 분리하여 별도의 테스트를 만든다.
🧷 저장소에 코드를 넣기 전에 자동으로 모든 테스트를 수행하도록 하는 훅을 구현하는 것도 좋은 생각이다.
🆎 테스트 함수의 이름은 테스트가 실패할 때에나 보이므로, 길고 서술적인 이름을 사용한다.
👩💻 무언가 잘못되었거나 뜯어고쳐야 하는 경우, 괜찮은 테스트 셋이 있다면 유지보수 담당자들은 그 테스트 셋에 전적으로 의지할 것이다.
class TestDataset():
test_data = [
('a', 'Red'),
('b', 'Red'),
('c', 'Blue')
]
test_data_result = [
['a', 'b'],
['c']
]
def get_test_data(self):
# DB에서 실제 데이터 조회 후 출력
...
import unittest
from unittest.mock import MagicMock, patch
from test_dataset import TestDataset as dataset
from my_module import function_to_test
class ProcessTest(unittest.TestCase):
...
# 1.
@patch('sqlalchemy.engine.Connection.execute')
def test_decompose_by_color(self, mock_execute):
# 2.
MockResultProxy = namedtuple('MockResultProxy', 'name, color')
mock_execute.return_value = [MockResultProxy(name, color) for name, color in dataset.test_data]
self.result = function_to_test([name for name, color in dataset.test_data])
# 3.
test_result = map(sorted, self.result)
test_data_result = map(sorted, dataset.test_data_result)
# 4.
self.assertCountEqual(test_result, test_data_result)
@patch
데코레이터는 인자로 받은 객체를 mocking해 데코레이팅한 함수에 인자로 넘긴다. 즉, test_decompose_by_color
함수는 sqlalchemy.engine.Connection.execute
를 mocking한 MagicMock 객체를 mock_execute
라는 이름으로 받게 된다. sqlalchemy.engine.Connection.execute
호출이 실제로 이루어지지 않고, 대신 호출된 MagicMock 객체가 return_value로 받은 값을 반환한다. 나는 sqlalchemy에서 쿼리의 결과로 반환하는 ResultProxy값과 유사하게 namedtuple을 만들어 넣어주었다.