개요

  • 리팩토링을 하고자 할 때 견고한 테스트는 필수 조건이다.
  • 테스트케이스 작성도 업무라는 인식을 가져야 한다. 테스트를 위해 별도의 코드를 작성해야 하므로
    이것을 꺼리는 개발자가 아직도 많다. 경우에 따라서 테스트 코드가 아주 커질 수도 있다.

자체 테스트 코드의 가치

  • 클래스에 메소드가 추가될 때마다 테스트 메소드도 추가하고 테스트를 실시한다.
  • 테스트 주기가 짧을수록 디버깅에 많은 시간이 허비하지 않는다. 그 만큼 생산성이 높아진다.
  • 한 벌의 테스트는 버그를 찾는 데 걸리는 시간을 단축시키는 강력한 버그 탐지기이다.
  • TestCase만으로도 어떤 기능이 있는 지 파악할 수 있다.

TestCase 를 작성하지 않는 변명

  • TestCase 작성에 많은 시간이 걸린다.
    • 자신이나 다른 사람이 작성한 코드를 디버깅하는 데 드는 비용이 훨씬 더 크다.
    • 잘 동작할거라고 생각했지만 중대한 버그로 코드를 다시 뜯어고치는 데 시간이 더 많이 든다.
    • 버그 위치를 찾는 데 쏟는 시간과 에너지가 크다.
  • Test 실행이 오래 걸린다.
    • 적절한 크기로 조각내어 자주 테스트한다.
    • 실행시간이 오래 걸리는 것은 오래 걸리는 것끼리 묶고 신속하게 테스트할 수 있는 그것끼리 묶어서 테스트한다.(가령,DB와의 테스트)
    • 반드시 하루에 한 번이상은 테스트한다.

JUnit 테스트 프레임워크

Erich Gamma와 Kent Beck에 의해 개발된 오픈 소스 프레임워크이다.
그림4.1 Test 구조(Composite Pattern)

  1. Fixture(테스트 장치)를 설정한다. (setUp메소드)
  2. setUp은 객체를 생성하고 tearDown은 객체를 제거한다.
    setUp은 Fixtrue를 설정하고 tearDown은 Fixture를 초기화시킨다.
    Fixtrue 초기화가 필요없다면 tearDown()은 구현하지 않는다.
  • TestSuite: TestCase 나 TestSuite의 집합체이다.

단위 테스트와 기능 테스트(Unit and functional tests)

  • 단위 테스트(Unit Test): 범위가 매우 한정적이다.
    1. 각각의 테스트 클래스는 하나의 패키지 내에서 작동한다.
    2. 다른 패키지에 대한 인터페이스는 테스트 하지만 그 이상은 잘 작동되는 것으로 간주한다.
  • 기능 테스트(Functional Test): 소프트웨어 전체가 제대로 동작하는 지 확인하기 위해 작성한다.
    보통 전체 시스템을 하나의 블랙박스로 다룬다. GUI를 통해 테스트하고 특정 입력에 대해서 데이터가 어떻게 변하는 지를 본다.
    1. 버그 발견 시 코드를 수정해야 하고 반드시 버그가 밖으로 드러나도록 단위 테스트를 추가한다.

테스트 추가하기

  1. 완전한 테스트를 실행시키기 않는 것보다는 불완전한 테스트라도 작성하고 실행시키는 것이 더 낮다.
  2. 뭔가 잘못 되었을 때에는 경계 조건을 생각하고 테스트를 경계 조건에 집중하라.
  3. 잘못될거라고 예상될 때 적절한 예외(Exception)이 발생하는 지도 꼭 테스트를 하라.
  4. 테스트로 모든 버그를 잡을 수 없다는 걱정 때문에 테스트 작성하는 멈추지 마라.
    실제로는 대부분의 버그를 잡을 수 있다.

예제

  • Sample Data(data.txt)

Bradman	99.94	52	80	10	6996	334	29
Pollock	60.97	23	41	4	2256	274	7
Headly	60.83	22	40	4	2256	270*	10
Sutcliffe	60.73	54	84	9	4555	194	16

  • JUnit 3.8.1 에서 테스트했다.

package com.oracleclub.refactoring;


import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;


public class FileReaderTest extends TestCase {

	private FileReader _input;
	private FileReader _empty;

	public FileReaderTest(String name) {
		super(name);
	}
	
	protected void setUp() {
		try {
			_input = new FileReader("D:\\dev\\workspace\\book-refactoring\\data.txt");
			_empty = newEmptyFile();
		} catch(FileNotFoundException e) {
			throw new RuntimeException("unable to open test file");
		} catch(IOException ioe) {
			throw new RuntimeException(ioe.toString());
		}
	}

	private FileReader newEmptyFile() throws IOException {
		File empty = new File("D:\\dev\\workspace\\book-refactoring\\empty.txt");
		FileOutputStream out = new FileOutputStream(empty);
		out.close();
		return new FileReader(empty);
	}

	protected void tearDown() {
		try {
			_input.close();
			
		} catch(IOException e) {
			throw new RuntimeException("error on closing test file");
		}
	}

	public void testRead() throws IOException {
		char ch = '&';
//		_input.close();
		for (int i=0; i<4; i++) {
			ch = (char) _input.read();
		}
//		assertTrue('d'==ch);
//		assert('m' == ch);		//에러가 발생하지 않는다.
		assertEquals('d', ch);	//메시지를 명확히 보여준다.
	}
	
	public static Test suite() {
		TestSuite suite = new TestSuite();
		suite.addTest(new FileReaderTest("testRead"));
		suite.addTest(new FileReaderTest("testReadAtEnd"));
		suite.addTest(new FileReaderTest("testReadBoundaries"));
		suite.addTest(new FileReaderTest("testEmptyRead"));
		suite.addTest(new FileReaderTest("testReadAfterClose"));
		return suite;
	}
	
	public void testReadAtEnd() throws IOException {
		int ch = -1234;
		for (int i=0; i<141; i++) {
			ch = _input.read();
			
		}
		
		assertEquals("read at end", -1, ch);
	}
	
	public void testReadBoundaries() throws IOException {
		assertEquals("read first char", 'B', (char) _input.read());
		int ch = 0;
		for (int i=0; i<140; i++) {
			ch = _input.read();
		}
//		assertEquals("read last char", '6', ch);
//		assertEquals("read at end", -1, _input.read());
//		assertEquals("read at end", -1, _input.read());
	}
	
	public void testEmptyRead() throws IOException {
		assertEquals(-1, _empty.read());
	}
	
	public void testReadAfterClose() throws IOException {
		_input.close();
		try {
			_input.read();
			fail("no exception for read past end");
			
		} catch(IOException e) { }
	}
	
}

JUnit4.0에서의 간단한 예제

여기서는 눈에 띄는 차이만 간단히 살펴보고 자세한 설명은 다음으로 미루겠다.


package com.oracleclub.refactoring;


import static org.junit.Assert.assertEquals;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;


public class FileReaderTest4 {

	private FileReader _input;
	
	@Before
	public void setUp() {
		try {
			_input = new FileReader("D:\\dev\\workspace\\book-refactoring\\data.txt");
		} catch(FileNotFoundException e) {
			throw new RuntimeException("unable to open test file");
		}
	}

		
	@After
	public void tearDown() {
		try {
			_input.close();
			
		} catch(IOException e) {
			throw new RuntimeException("error on closing test file");
		}
	}
	
	@Test
	public void read() throws IOException {
		char ch = '&';
//		_input.close();
		for (int i=0; i<4; i++) {
			ch = (char) _input.read();
		}
//		assertTrue('d'==ch);
//		assert('m' == ch);		//에러가 발생하지 않는다.
		assertEquals('d', ch);	//메시지를 명확히 보여준다.
	}
	
}

  • TestCase를 상속받지 않아도 된다.
  • assert 메소드를 static으로 import한다.
  • @Test Annotation

문서에 대하여