3 분 소요

1. 테스트 코드 작성 이유

오늘은 어제에 이어서 리팩토링을 하기 위해 반드시 필요한 테스트 코드에 대해서 학습하였다. 리팩토링을 제대로 수행하고 또 정상적으로 진행되었는지를 확인하려면 테스트 코드가 반드시 뒷받침되어야한다. 이미 테스트라는 관점에서 생각했을 때 당연하게 생각될지도 모른다. 그렇다면 그 이유에 대해서 조금 구체적으로 소개하고자한다.

리팩토링은 단순히 코드의 구조를 변경하는 작업이기 때문에 리팩토링을 수행하기 전에 이미 코드 자체에서 오류가 발생했다면 리팩토링 후에도 오류가 여전히 남아있는 상태로 유지시켜야한다. (리팩토링 도중 에러를 수정하는 작업은 리팩토링을 했다고 말할 수 없다) 하지만 보통 개발자가 어떠한 작업 도중 코드에서 에러가 난다면 이를 수정하려고 하는 것이 일반적이다. 또한 수정하지 않으려고해도 해당 에러가 리팩토링 작업에 의해 발생한 에러인지 원래 발생한 에러인지를 미리 알고 있어야하기 때문에 더 복잡한 작업이 될 수 있다. 이러한 문제를 원천 차단하기 위해 시작부터 에러 없는 코드로 작업하는 것이 가장 효율적이고, 에러가 발생하지 않았다는 사실을 뒷받침해줄 수 있는 것이 바로 테스트 코드이다.

리팩토링의 관점 말고도 테스트 코드를 작성하면 얻게되는 또 다른 이점도 있다. 프로그래밍을 수행하기 전 구현할 기능에 대해 필요한 테스트 코드를 생각해보면 기능의 구현 방향과 생각해야할 여러 제약 조건에 대해 미리 고민하게 된다. 이러한 이점을 극대화하기 위해 등장한 기법이 TDD(Test-Driven Development)라고 불리는 테스트 주도 개발 방법론이다. 따라서 TDD 방법론을 활용하여 개발을 진행하게되면 견고한 코드를 작성할 수 있고, 디버깅이 용이해져 개발 시간을 효율적으로 단축할 수 있다는 장점이 있다.

2. 테스트 코드 작성법

내가 작성한 코드를 적으로 돌려라!

기본적인 테스트 코드 작성하기

이 책에서는 모카라는 테스트 프레임워크를 사용해 테스트 코드에 대한 예시를 소개해주고 있다. 내가 나중에 사용하고 싶은 Jest 프레임워크와 유사하게 테스트 코드를 블록 단위로 나누는 구조를 가지고 있어서 이해하는데 크게 어렵지 않았다.

테스트 코드를 작성하기 위해서는 가장 먼저 픽스처라고 부르는 테스트에 필요한 데이터와 객체를 설정해야한다. 이후 설정한 테스트 데이터와 객체를 가지고 테스트를 원하는 로직을 수행하고 난 결과를 검증한다.

일반적으로 테스트를 실행하는 it 구문에서는 하나의 결과에 대해서만 검증하는 것이 가장 좋은 방법이다.


describe('province', function(){ // 테스트 블록
    it('shortfall', function(){ // 테스트 실행
        const asia = new Province(sampleProvinceData()); // 1. 픽스처 설정
        assert.equal(asia.shortfall, 5); // 2. 결과 검증
    });
});

작성한 모든 테스트가 성공한다면 좋은 일이지만, 테스트가 작성자의 의도와 다른 방식으로 테스트를 수행했을 가능성을 배제할 수 없기 때문에 테스트를 진행하는 로직 자체를 수정해서 실패가 출력되는지 확인한다.

테스트를 진행할 때 에러와 실패를 명확히 구분해서 이야기 하는 것이 좋다.

에러 : 코드 상에서 문제가 발생하여 테스트를 정상적으로 진행할 수 없는 경우
실패 : 코드를 정상적으로 실행했지만 원하는 결과값이 아닌 경우

테스트 결과 검증하기

테스트 결과를 검증하는 assertion 라이브러리에는 굉장히 다양한 종류가 있다. 해당 라이브러리를 사용하면 assert 문으로 코드를 검증할 수 있고, 또는 expect 문으로 코드를 검증할 수 있다.


// asia 객체의 shortfall 속성 결과값이 5인지 비교
assert.equal(asia.shortfall, 5);
expect(asia.shortfall).equal(5);

전처리문을 활용하여 테스트 추가하기

하나의 테스트를 작성한 후 새로운 테스트를 추가할 때 기존에 선언한 픽스처를 재사용하고 싶은 마음이 들 수 있다. 이것은 좋은 생각이지만, 한편으로는 위험한 생각이 될 수 있다. 불변성을 유지하여 값이 수정될 수 없음을 보장하는 픽스처는 재사용을 해도 문제가 되지 않는다. 하지만 참조값을 가진 픽스처는 내부 값이 수정될 수 있기 때문에 한 테스트에서 내부 값을 수정하게 되면 다음 테스트에 영향을 미치는 문제가 발생할 수 있다. 결국 두 테스트 코드 사이에 종속 관계가 형성되는 것이다.


describe('province', function(){ 
    it('shortfall', function(){ 
        const asia = new Province(sampleProvinceData()); // 참조 픽스처
        // 이 부분에서 profit에 대한 값 수정이 발생한다면
        expect(asia.shortfall).equal(5);
    });
    it('profit', function(){
        expect(asia.profit).equal(230); // 여기에서 에러가 발생할 수 있음
    });
});

이러한 문제를 막기 위해서 beforeEach 라고 부르는 전처리문을 활용한다. beforeEach는 테스트 코드가 실행되기 전에 가장 먼저 실행되는 구문으로 서로 다른 각각의 테스트 코드가 실행될 때마다 동작한다. 따라서 beforeEach 문에서 픽스처를 선언하는 방식으로 픽스처를 독립적으로 활용할 수 있게 된다. 또한, 이렇게 작업할 시 beforeEach 부분만 살펴보면 테스트 코드가 어떤 자원을 활용하는지 손쉽게 파악이 가능하다.


describe('province', function(){
    let asia;
    beforeEach(function(){
        asia = new Province(sampleProvinceData()); // 독립 픽스처 설정
    })
    it('shortfall', function(){         
        // 이 부분에서 profit에 대한 값 수정이 발생해도
        expect(asia.shortfall).equal(5);
    });
    it('profit', function(){
        expect(asia.profit).equal(230); // 이 픽스처는 독립적이므로 영향이 없다.
    });
});

경계범위(예외처리) 고민

테스트의 결과를 검증할 때는 작성자가 원하는 결과값을 미리 생각해야한다. 하지만 이 과정에 있어서 원하는 결과값이 작성한 코드의 의미와 부합하는지를 생각해볼 필요가 있다.

예를 들어, 어떤 제품 팔고 남은 수량을 계산하는 로직이 정상적으로 수행되는지를 테스트한다고 가정해보자. 결과값이 0이상의 값이 나온다면 다행이지만, 음수가 나오는 상황이 있다면 이를 처리하는 예외처리 로직이 없거나 정상적으로 예외를 처리하지 않는다는 사실을 알 수 있게 된다. 이처럼 결과값이 나올 수 있는 경계범위를 고민하고 테스트를 수행함으로써 테스트를 진행한 로직 내에 예외처리 로직이 부족한지에 대해 확인할 수 있다.

3. 후기

이번 시간에는 테스트 코드의 필요성과 대략적인 작성 방법, 고려 사항 등에 대해서 알아보는 시간을 가졌다. 테스트 코드는 많은 장점을 가지고 있지만 작성한 모든 코드에 대해서 테스트를 진행할 필요는 없다. 본인이 구현한 기능에서 확신을 가질 수 없는 위험한 부분이나 처리 과정이 복잡하여 검증 로직이 필요하다고 생각하는 부분 위주로 테스트를 진행하는 것이 제일 효율적이다.

위에서 설명한 내용들은 테스트 코드를 작성하기 위한 가장 기초적인 내용으로 이외에도 테스트 코드와 관련된 수많은 기능과 고려할 점들이 있다. 따라서 이후에는 JavaScript 테스트를 위해 가장 대중적으로 사용하고 있는 Jest 프레임워크에 대한 활용법을 학습하여 추후의 프로젝트에서 테스트 코드를 능숙하게 작성하는 능력을 키우고, TDD 개발 방법론에 대한 내용 조사를 통해 테스트를 적극적으로 활용하는 기반을 마련할 예정이다.