1. 테스트 주도 개발
테스트의 중요성은 과거에 비해서 점차 커지고 있습니다. 크게 두 가지를 이유로 꼽고 싶습니다.
첫 번째는 과거에 비해서 프로그램 규모가 커졌습니다. 과거에는 한두 명이서 프로그램을 만들었다면, 요즘은 수십 명이서 한 프로젝트에서 협업합니다. 많은 프로그래머가 각자 만든 코드들의 접점이 늘어나게 되므로 예기치 못한 버그도 늘 수밖에 없습니다.
두 번째로 과거에 비해 고가용성(High Availability)에 대한 요구사항이 높아졌습니다. 가용성이란 프로그램이나 웹 서비스가 얼마나 오랫동안 정상 동작하는가를 의미합니다. 대부분 유명한 웹 서비스는 주 7일 24시간 서비스를 기본으로 제공합니다. 무중단 서비스에 대한 사용자 요구가 늘어나면서 서비스를 중단으로 이르게 할 수 있는 치명적인 버그를 줄이는 게 서비스 성패에 큰 역할을 하게 됐습니다.
이런 두 가지 이유로 과거에 비해서 더 많은 그리고 더 촘촘한 테스트가 필요하게 됐습니다.
테스트는 크게 블랙박스 테스트와 화이트박스 테스트로 구분할 수 있습니다.
블랙박스 테스트
블랙박스 테스트란 제품 내부를 오픈하지 않은 상태에서 진행되는 테스트를 말합니다. 사용자 입장에서 테스트한다고 해서 사용성 테스트(Usability Test)라고 하기도 합니다. 블랙박스 테스트는 프로그램 내부 코드를 직접 검증하는 게 아니라 프로그램을 실행한 상태로 실행 동작을 검사하는 방식입니다. 프로그래머보다는 전문 테스터, QV, QA 직군에서 주로 담당합니다.
화이트박스 테스트
화이트박스 테스트는 프로그램 내부 코드를 직접 검증하는 방식입니다. 유닛 테스트(Unit Test, 단위 테스트)라고 부릅니다. 이 테스트는 프로그래머가 직접 테스트 코드를 작성해서 내부 테스트를 검사하는 방식입니다.
블랙박스 테스트는 내부 코드를 검증하지 않고 제품 전체의 사용성을 검사하는 방식이기 때문에 코드 내부에 잠재되어 있는 버그를 찾는 데 어려움이 있습니다. 반면 화이트박스 테스트는 코드를 직접 검사할 수 있지만 사용자 입장에서 전체 서비스를 검사하는 데 어려움이 있습니다. 따라서 이 두 방식 모두 많이 그리고 꼼꼼하게 진행해야 더 좋은 품질의 제품을 만들 수 있게 됩니다.
화이트박스 테스트에 대해서 좀 더 알아보겠습니다. 테스트는 일반적으로 다음과 같이 진행됩니다.
코드 작성 → 테스트하고 버그를 발견 → 코드 수정하는 형태가 전통적인 방식이었습니다. 하지만 이런 방식에는 많은 문제점을 가지고 있습니다.
첫 번째로 빈약한 테스트 케이스가 문제입니다. 테스트는 많을수록 촘촘할수록 더 좋습니다. 코드 작성 이후에 테스트 코드를 작성하다 보면 메인 시나리오에 의존해 테스트하고 예외 상황이나 경계 체크boundary check가 무시되기 쉽습니다.
예를 들어 쇼핑몰을 만든다고 하면 장바구니에 물건을 담아서 결제하는 메인 시나리오를 검증하는 테스트 코드만 만들고, 장바구니에 물건을 담은 상태에서 브라우저를 종료하거나 또는 결제 도중에 장바구니에 담긴 상품이 품절되는 등 예외 상황을 테스트하지 않아서 실제 서비스가 오픈됐을 때 많은 문제가 발생하곤 합니다.
두 번째로 테스트 통과를 목적으로 하는 형식적인 테스트 코드를 작성하기 십상입니다. 테스트의 중요성이 증가되면서 회사 정책으로 테스트 코드 작성을 강제하는 회사가 늘었습니다. 하지만 테스트가 프로젝트 막바지에 몰려있다 보니 마감에 떠밀려 테스트 통과만을 목적으로 테스트 코드를 작성하는 문제가 생기게 됐습니다. 이런 테스트는 효용성이 떨어지게 마련입니다.
테스트 주도 개발
테스트 주도 개발(Test Driven Development, TDD)은 이런 문제를 해결하는 대안입니다. 테스트 주도 개발은 테스트 코드 작성 시기를 과감하게 코드 작성 이전으로 옮긴 방식입니다.
제일 먼저 테스트 코드부터 작성합니다. 구현하기 전에 테스트 코드부터 작성하기 때문에 당연히 테스트가 실패합니다. 방금 작성한 테스트를 성공시키는 코드를 작성해서 테스트를 성공시킵니다. 그리고 개선 작업을 통해 코드를 개선합니다. 개선은 SOLID 원칙에 입각해 진행합니다. 이것을 리팩터링(Refactoring)이라고 말합니다. 그 뒤 새로운 테스트 코드를 작성해서 다시 테스트 실패를 만듭니다. 이 과정을 계속 반복하는 게 테스트 주도 개발입니다.
그렇다면 테스트 주도 개발에 어떤 이점이 있는 걸까요?
첫 번째, 테스트 코드가 자연적으로 촘촘해집니다. 테스트 코드를 작성하고 성공으로 만드는 코드를 작성하고 다시 테스트 코드를 작성하는 걸 반복하기 때문에 테스트 코드가 자연스럽게 늘어나게되고,허술한테스트코드없이다양한경우에대해서검증할수있습니다.
두 번째, ‘작은 목표 설정 → 실패 → 달성 → 달성 강화 → 새로운 작은 목표 설정’ 절차를 따르기 때문에 개발 자체가 재밌어집니다.
⟪Tucker의 Go 언어 프로그래밍 2판⟫ ex26.1에서는 9의 제곱을 검사하는 TestSquare1을 만들어서 테스트했습니다. square( ) 함수가 고정값 81을 반환하도록 되어 있었기 때문에 TestSquare1은 통과될 수 있었습니다. 그래서 이번에는 3의 제곱을 검사하는 TestSquare2를 만들었습니다. 코드를 수정하기 앞서 테스트 케이스부터 만든 겁니다. square( ) 함수는 여전히 고정값 81을 반환하기 때문에 테스트가 실패합니다. 그래서 square( ) 함수를 고쳐서 두 테스트 케이스 모두 통과될 수 있도록 수정했습니다. 이 과정은 테스트 코드 작성 → 코드 수정 → 다시 테스트 코드 작성으로 이어집니다. 이러한 과정을 바로 테스트 주도 개발이라고 합니다.
테스트 주도 개발이 등장하고 난 뒤 많은 화제를 불러일으켰습니다. 테스트 주도 개발에 장점이 많지만 모든 영역에서 적용시키기 어렵다는 단점도 있습니다. 하지만 가능한 영역에서 테스트 주도 개발을 적용한다면 좋은 코드를 만드는 원동력이 될 수 있습니다. 거듭 강조하지만 테스트는 많을수록 촘촘할수록 좋습니다.
2. 테스트 코드 작성하기
Go 언어는 테스트 코드 작성과 실행을 언어 자체에서 지원합니다. 빠르고 손쉽게 테스트 코드를 작성할 수 있어 버그를 사전에 막는 데 효과적입니다. Go 언어로 테스트 코드를 한번 작성해보겠습니다.
3가지 표현 규약을 따라 테스트 코드를 작성해야 하며, go test 명령으로 실행합니다. 작성 규약은 다음과 같습니다.
- 파일명이 _test.go로 끝나야 합니다.
- testing 패키지를 임포트해야 합니다.
- 테스트 코드는 func TestXxxx(t *testing.T) 형태이어야 합니다.
각 규약을 다음과 같이 해석할 수 있습니다.
- 테스트 코드는 파일명이 _test.go로 끝나는 파일 안에 존재해야 합니다.
- 테스트 코드를 작성하려면 import “testing”으로 testing 패키지를 가져와야 합니다.
- 테스트 코드들은 모두 함수로 묶여 있어야 하고 함수명은 반드시 Test로 시작해야 합니다. Test 다음에 나오는 첫 글자는 대문자여야 합니다. 또한 함수 매개변수는 t *testing.T 하나만 존재해야 합니다.
작성하기
표현 규약에 맞춰 테스트 코드를 작성해보겠습니다. 테스트 대상 코드를 작성 후 테스트 코드를 작성하겠습니다.
테스트 대상 코드 81을 반환하는 함수 | ch26/ex26.1/ex26.1.go
- https://github.com/tuckersGo/mustHaveGo2/blob/main/ch26/ex26.1/ex26.1.go
package main
import "fmt"
func square(x int) int {
return 81
}
func main() {
fmt.Printf("9 * 9 = %d\n", square(9))
}
9 * 9 = 81
테스트 코드 9의 제곱값이 81임을 테스트하는 코드 | ch26/ex26.1/ex26_1_test.go
- https://github.com/tuckersGo/mustHaveGo2/blob/main/ch26/ex26.1/ex26.1.go
package main
import "testing"
func TestSquare1(t *testing.T) {
rst := square(9)
if rst != 81 {
t.Errorf("square(9) should be 81 but square(9) returns %d", rst) // ❶
}
}
❶ t.Errorf( ) 메서드에 테스트 실패 시 실패를 알리고 실패 메시지를 넣을 수 있습니다. testing.T 객체의 Error( )와 Fail( ) 메서드를 이용해서 테스트 실패를 알릴 수 있습니다. Error( )는 테스트가 실패하면 모든 테스트를 중단하지만, Fail( )은 테스트가 실패해도 다른 테스트들을 계속 진행합니다.
테스트 코드를 작성하고 코드를 작성한 폴더의 터미널 창에서 go test를 실행합니다. 또는 비주얼 스튜디오 코드에서 테스트 코드를 작성하면 자동으로 나오는 run package tests(테스트 실행하기) 버튼을 클릭해 실행할 수도 있습니다.
출력 결과는 다음과 같습니다.
PASS
ok ex26.1 0.179s
테스트가 모두 통과됐고 총 0.179초가 걸렸습니다. 실행 시간은 머신 성능에 따라서 달라질 수 있습니다.
테스트 코드를 하나 더 추가해보겠습니다. ex28_1_test.go 파일 아래쪽에 다음 코드를 추가합니다.
테스트 코드 3의 제곱값이 9임을 테스트하는 코드
func TestSquare2(t *testing.T) {
rst := square(3)
if rst != 9 {
t.Errorf("square(3) should be 9 but square(3) returns %d", rst)
}
}
터미널에서 go test 명령으로 테스트를 실행합시다. 출력 결과는 다음과 같습니다.
--- FAIL: TestSquare2 (0.00s)
ex28_1_test.go:15: square(3) should be 9 but square(3) returns 81
FAIL
exit status 1
FAIL ex26.1 0.193s
TestSquare2가 실패했고, 9가 나와야 하는데 81이 나온 것을 실패 메시지에서 확인할 수 있습니다. 그 이유는 ex26.1.go 파일에서 square( ) 함수가 항상 81을 반환하도록 했기 때문입니다. square( ) 함수를 다음과 같이 고쳐서 다시 테스트를 돌려보겠습니다.
테스트 대상 코드 square() 함수가 인수의 제곱을 반환하게 수정된 코드
func square(x int) int {
return x * x
}
그러면 모든 테스트가 통과됐다는 메시지가 출력됩니다.
PASS
ok ex26.1 0.165s