테스트 코드의 여러가지 유형

in #programming8 years ago

Facebook TDD Django 스터디 그룹을 위해 작성된 글입니다.
교재 Test driven development with Python (번역서 : 파이선을 이용한 클린코드를 위한 테스트 주도 개발)
http://www.obeythetestinggoat.com/

테스트의 중요성을 인식하면서도 테스트 코딩을 선듯 하지 못하는 경우가 많다.

  • 어떻게 테스트를 작성하는지 모르겠다.
  • 테스트 코드를 작성하는 것은 너무 어렵다.
  • 테스트를 만들 시간이 없다.
  • 테스팅은 나의 일... 코드가 뭐가 필요?
  • 내 코드는 너무나 간단해서 테스트할 필요가...

테스트의 장접이나 TDD 의 유용함에 대해서 언급한 글을 이미 많으니,
여기서는 여러가지 테스트 달기 유형에 대해서 소개한다.

1. 함수의 Unit Test

y = f(x) 에서 x 값에 따라서 정확한 y 값이 나오는지 확인

1) 너무나 뻔한 예제

def add(x, y):
    return x + y

class AddTest(TestCase):
    def test_add(self):
        self.assertEquals(5, add(2+3))

2) 좀 덜 뻔한 예제

입력값에 따른 출력값의 테스트
1부터 N 까지의 합을 구하는 함수 n(n+1)/2 는 1+2+...+n 을 해봄으로써 테스트 할 수 있다.
코드는 생략

1), 2)는 좋은 예제이기는 하나 실제로 사용하는 경우는 많지 않다.
우리가 하는 코딩은 수학적인 내용이 아니다.

3) 알고리즘에 대한 테스트

엄청나게 빠른 정렬 알고리즘을 발견했어
그럼 이 알고리즘을 어떻게 테스트하지? 유닛테스트가 있잖아.
우리가 알고 있는 Quick 정렬로 정렬한 후 결과 비교해 보면 된다.
이거 뭐 이상하잖아 어짜피 둘다 정렬인데, 그렇다면

  1. 입력 리스트와 출력 리스트 갯수가 동일
  2. 출력 리스트에 대해서 오름차순
def very_fast_sort(list):
    pass

def test_sort(self):
    list = [2,3,1,4,5]
    sorted = very_fast_sort(list)
    self.assertEqual(list.length = sorted.length)
    i = 1
    while (i < list.length):
        if sorted[i - 1] > sorted[i]:
            fail(“Order is not matched”)

그래 이것이 정렬에 대한 테스트지...
정렬의 정의가 맞는지 검사하는 것이다.

2. 객체간의 상관관계 테스트

실제 코딩의 대부분은 실생활의 관계를 코딩한다.
객체 지향 프로그래밍은 A --> B --> C 메시지를 던지는 프로그래밍이다.
A 가 B 에 메시지를 던졌는데 C 의 행동이 내가 예상했던 행동인가?

  • 로그인한 사용자는 특정 url 에 접근할 수 있다.
  • 로그인 하지 않은 사용자는 redirect 되어야 한다.
  • 사용자가 등록되면, 어떤 테이블에 정보가 입력되어야 하며, 이메일이 전송되어야 한다.
  • 이메일이 전송되면, 사용자 테이블이 업데이트 되어야 한다.

로그인 관련 테스트를 코드화 하면 다음과 같다.

def test_login_client_can_go_to_special_page(self):
    client = Client()
    client.post(‘/login/', { ‘user': ‘username', 'password': ‘password', })
    response = client.get(‘/special')
    self.assertEqual(response.status_code, 200)

def test_not_login_client_cannot_go_to_special_page(self):
    client = Client()
    response = client.get(‘/special')
    self.assertEqual(response.status_code, 301)

A 다 해 놓은 걸 뭐하러 테스트 하라는 거야?
B 너무 뻔한것 같나?
A 아니 이게 테스트야 스펙이야?
B 스펙이 있기는 있나? 문서로 된게 있어?
A 없는데 머리속에 다 있어...

테스트의 기본은 내가 프로그래밍 한 방식으로 원하는 결과가 나오느냐를 검사하는 것이다.
프로그래밍의 본질이 요구사항을 코드화 하는 것이라면,
테스트 코딩은 프로그래밍의 본질을 가장 잘 지원하는 도구일 것이다.
요구사항을 코드화 하고 그걸 기반으로 개발한다면 그것을 TDD 라고 할 수 있다.

테스트 작성은 요구사항을 실제로 코딩으로 옮기기 위해서 추상화된 요구사항을 구체화 하는 과정이다.
객체와 함수의 행동을 테스트 하는 것이므로, 객체와 함수가 복잡하다면 테스트하기 어렵다.
따라서 테스트를 달기 위해서 객체와 코드를 간결하게 할 수 밖에 없다.
코드가 테스트하기 적합하다면 이 코드는 Testable 코드이다.
Testable 한 함수는 짧고 한가지 일을 해야하며 같은일을 하는 다른 함수는 없어야 한다.

3. 리팩토링을 위한 안전망

테스트는 리팩토링에 대한 안전함을 제공한다. (테스트가 있다고 리팩토링을 신뢰한다는 의미는 아니다)
유명한 마틴 파울러의 리팩토링 책 Chap1 예제에 대한 테스트 코드를 Python 으로 작성하면 아래와 같을 것이다.

class TestCustomer(unittest.TestCase):
    def test(self):
        m1 = Movie('Benher', Movie.REGULAR)
        m2 = Movie('Shurek', Movie.CHILDREN)
        m3 = Movie('Avengers', Movie.NEW_RELEASE)
        r1 = Rental(m1, 2)
        r2 = Rental(m2, 3)
        r3 = Rental(m3, 5)
        customer = Customer(‘John')
        customer.add_rental(r1)
        customer.add_rental(r2)
        customer.add_rental(r3)
        self.assertEqual(customer.statement(), '''Rental Record for John
\tBenher\t2
\tShurek\t1.5
\tAvengers\t15
Amount owed is 18.5
You earned 4 frequent renter points''')

레거시 코드를 다룰 때는 유닛테스트를 달기 어려운 경우가 많다.
함수가 여러가지 일을 하며, 구조가 복잡하여 Testable 하지 않기 때문이다.
이러한 경우 리팩토링을 하면서 유닛테스트를 다는 것이 좋다.
리팩토링을 위한 테스트는 전체를 아우를 수 있는 테스트 코드 작성한다.
전체의 입력 출력에 대한 테스트 코드를 다는 것이 좋을 것이다.

4. 테스트 훈련

소프트웨어 디자인, 테스트, 리팩토링은 한 몸이다.
글을 쓸 때, 좋은 구조와 간결한 글을 쓰는 것이 훈련을 통해서 가능하듯이
테스트, 리팩토링은 훈련을 통해서 가능하다.

새로운 라이브러리를 배울 때, 그냥 하지 말고 해보면서 해본다.
list.append(a) 했을 때 return 값은 None 이며, list 는 변한다.
아래와 같이 테스트 코드를 작성하면 좀 더 이해가 쉬울 것이다.

class ListTest(unittest.TestCase):
    def test_list_append(self):
        list = [1, 2, 3,]
        ret = list.append(4)
        self.assertEqual(ret, None)
        self.assertEqual(list, [1, 2, 3, 4,])

append 외 다른 함수들도 테스트로 만들어 보자.

코딩은 작문에 가깝다. 쉽고 누구나 이해할 수 있는 작문은 처음부터 되는 것은 아니다.
반복적인 연습을 통해 이룩될 수 있으며, 이는 하면 할 수록 향상되며 결코 실패하지 않는다.

Coin Marketplace

STEEM 0.22
TRX 0.21
JST 0.035
BTC 98705.91
ETH 3342.47
USDT 1.00
SBD 3.14