'테스팅'이란 응용 프로그램 또는 시스템(구성요소 포함)의 동작과 성능, 안정성이
요구하는 수준을 만족하는지 확인하기 위해 결함을 발견하는 과정이라고 말할 수 있습니다.
현재의 테스팅 개념은 사용자의 기대 수준과 요구 사항에 맞게 구현되고 동작하는지를 확인하고 이를 통해 결함을 발견하고,
최종적으로 결함 데이터를 근간으로 개발 프로젝트의 리스크(Risk)에 대한 수치적인 판단 근거를
의사 결정권자(프로젝트 관리자 등)에게 전달하는 것을 말합니다.
개발 프로젝트 초기에 개발 중간 산출물(Work products)을 테스팅 관점에서 리뷰(Review)하고, 테스트 케이스를 미리 만드는 과정에서 결함을 발견하는 작업(결함 예방 활동)도 테스팅 활동의 중요한 부분이라고 말할 수 있습니다.
프로그램을 개발하기 전에 요구사항 등을 리뷰하는 것을 정적 테스트라고 하고,
프로그램 개발 이후에 실제 실행하면서 테스트하는 것을 동적 테스트라고 합니다.
프로그래밍 언어마다 테스트를 위한 프레임워크가 존재합니다.
이러한 도구들을 보통 xUnit이라고 말합니다. 자바언어의 경우는 JUnit이라고 말합니다.
직접 다운로드를 받는 것은 번거롭기 때문에 보통 빌드 도구인 Maven이나 Gradle을 이용해 다운로드 받아 사용합니다.Maven을 사용할 경우 pom.xml에 다음을 추가합니다.
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>버전</version>
<scope>test</scope>
</dependency>
CalculatorService.java
package org.edwith.webbe.calculatorcli;
public class CalculatorService {
public int plus(int value1, int value2) {
return value1 + value2;
}
public int minus(int value1, int value2) {
return value1 - value2;
}
public int multiple(int value1, int value2) {
return value1 * value2;
}
public int divide(int value1, int value2) throws ArithmeticException {
return value1 / value2;
}
}
CalculatorServiceTest.java
package org.edwith.webbe.calculatorcli;
import org.junit.Before;
import org.junit.Test;
import org.junit.Assert;
public class CalculatorServiceTest {
CalculatorService calculatorService;
@Before
public void init() {
this.calculatorService = new CalculatorService();
}
@Test
public void plus() throws Exception {
//given
int value1 = 10;
int value2 = 5;
//when
int result = calculatorService.plus(value1, value2);
//then
Assert.assertEquals(15, result); //결과가 15와 같을 경우에만 성공
}
@Test
public void divide() throws Exception {
//given
int value1 = 10;
int value2 = 5;
//when
int result = calculatorService.divide(value1, value2);
//then
Assert.assertEquals(2, result);
}
@Test
public void divideExceptionTest() throws Exception {
int value1 = 10;
int value2 = 0;
try {
calculatorService.divide(value1, value2);
} catch (ArithmeticException ae) {
Assert.assertTrue(true); //이 부분이 실행되었다면 성공
return; //메소드를 더이상 실행하지 않는다
}
Assert.assertFail(); // 이 부분이 실행되면 무조건 실패다
}
}
스프링 빈 컨테이너에서 관리하는 빈 객체를 테스트하는 방법
스프링 프레임워크를 사용하려면 관련 라이브러리들이 프로젝트에 추가 되야 합니다. pom.xml 파일을 다음과 같이 수정합니다.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.edwith.webbe</groupId>
<artifactId>calculatorcli</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<failOnMissingWebXml>false</failOnMissingWebXml>
<spring.version>5.2.3.RELEASE</spring.version>
</properties>
<dependencies>
<!-- junit 4.12 라이브러리를 추가합니다. -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- spring-context와 spring-test를 의존성에 추가합니다.-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
<!-- 사용할 JDK버전을 입력합니다. JDK 11을 사용할 경우에는 1.8대신에 11로 수정합니다.--><!-- 사용할 JDK버전을 입력합니다. JDK 11을 사용할 경우에는 1.8대신에 11로 수정합니다.-->
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>utf-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
수정을 하고 나서 Maven update를 수행합니다.
스프링 프레임워크를 사용하려면 설정 파일을 작성해야 합니다. 스프링 설정 파일은 xml파일이나 Java Config로 작성할 수 있다고 하였습니다. Java Config파일로 다음과 같이 작성해 보도록 하겠습니다.
package org.edwith.webbe.calculatorcli;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages = {"org.edwith.webbe.calculatorcli"})
public class ApplicationConfig {
}
클래스 위에 @Configuration 어노테이션이 붙어 있으면 스프링 설정 파일이라는 것을 의미합니다.
스프링 설정 파일은 스프링 빈 컨테이너인 ApplicationContext에서 읽어 들인다고 하였습니다.
@ComponentScan은 특정 패키지 이하에서 컴포넌트를 찾도록 합니다.
해당 어노테이션에 설정된 패키지 이하로부터 @Component, @Repository, @Service, @Controller, @RestController 등의 어노테이션이 붙은 클래스를 찾아 빈으로 등록합니다.
package org.edwith.webbe.calculatorcli;
import org.springframework.stereotype.Component;
@Component
public class CalculatorService {
public int plus(int value1, int value2) {
return value1 + value2;
}
public int minus(int value1, int value2) {
return value1 - value2;
}
public int multiple(int value1, int value2) {
return value1 * value2;
}
public int divide(int value1, int value2) throws ArithmeticException {
return value1 / value2;
}
}
스프링 빈 컨테이너에서 관리한다는 것은 개발자가 직접 인스턴스를 생성하지 않는다는 것을 의미합니다. 스프링 빈 컨테이너가 인스턴스를 생성해 관리한다는 것을 뜻합니다. 스프링 빈 컨테이너가 CalculatorService클래스를 찾아 빈으로 등록할 수 있도록 클래스 위에 @Component를 붙입니다.
위와 같이 작성했다면, 기존 클래스를 스프링 프레임워크에서 사용될 준비가 끝난 것입니다. CalculatorService 클래스를 이용하려면 다음과 같은 클래스를 작성해야 합니다.
package org.edwith.webbe.calculatorcli;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Main {
public static void main(String[] args) {
//ApplicationConfig.class 설정파일을 읽어들이는 ApplicationContext 객체를 생성합니다
//아래 한줄이 실행되면서 컴포넌트 스캔을 하고, 컴포넌트를 찾으면 인스턴스를 생성하여 ApplicationContext가 관리하게 됩니다.
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(ApplicationConfig.class);
//ApplicationContext가 관리하는 CalculatorService.class타입의 객체를 요청합니다
CalculatorService calculatorSerivce = applicationContext.getBean(CalculatorService.class);
//ApplicationContext로 부터 받은 객체를 이용하여 덧셈을 구합니다
System.out.println(calculatorSerivce.plus(10, 50));
}
}
기존 테스트 클래스는 테스트할 객체를 @Before가 붙은 메소드에서 초기화 하였습니다. 스프링 빈 컨테이너를 사용할 때는 개발자가 직접 인스턴스를 생성하면 안됩니다.
스프링 빈 컨테이너가 빈을 생성하고 관리하도록 하고, 그 빈을 테스트 해야합니다.
이를 위해서 스프링 프레임워크는 몇가지 특별한 기능을 제공합니다. 소스 코드를 아래와 같이 수정하도록 합니다.
package org.edwith.webbe.calculatorcli;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {ApplicationConfig.class})
public class CalculatorServiceTest {
@Autowired
CalculatorService calculatorService;
@Test
public void plus() throws Exception{
// given
int value1 = 10;
int value2 = 5;
// when
int result = calculatorService.plus(value1, value2);
// then
Assert.assertEquals(result, 15); // 결과와 15가 같을 경우에만 성공
}
@Test
public void divide() throws Exception{
// given
int value1 = 10;
int value2 = 5;
// when
int result = calculatorService.divide(value1, value2);
// then
Assert.assertEquals(result, 2); // 결과와 15가 같을 경우에만 성공
}
@Test
public void divideExceptionTest() throws Exception{
// given
int value1 = 10;
int value2 = 0;
try {
calculatorService.divide(value1, value2);
}catch (ArithmeticException ae){
Assert.assertTrue(true); // 이부분이 실행되었다면 성공
return; // 메소드를 더이상 실행하지 않는다.
}
Assert.assertTrue(false); // 이부분이 실행되면 무조건 실패다.
}
}
기존 테스트 클래스 위에 @RunWith(SpringJUnit4ClassRunner.class)를 붙입니다.
@RunWith 어노테이션은 JUnit이 제공하는 어노테이션입니다.
JUnit은 확장기능을 가지는데, 스프링에서는 JUnit을 확장하도록 SpringJUnit4ClassRunner.class를 제공합니다.
해당 코드는 JUnit이 테스트 코드를 실행할 때 스프링 빈 컨테이너가 내부적으로 생성되도록 합니다.
@ContextConfiguration(classes = {ApplicationConfig.class})은 내부적으로 생성된 스프링 빈 컨테이너가 사용할 설정파일을 지정할 때 사용합니다.
위에서 설명한 2줄이 테스트 클래스 위에 있으면, 테스트 클래스 자체가 빈(Bean)객체가 되어 스프링에서 관리되게 됩니다.
@Autowired
CalculatorService calculatorService;
CalcultorServiceTest 클래스가 빈으로 관리되면서, 스프링 빈 컨테이너는 CalculatorService를
주입(Inject)할 수 있게 됩니다. 이렇게 주입된 클래스를 테스트하면 됩니다.
테스트 결과는 기존의 클래스와 같은 것을 확인할 수 있습니다.
빈과 빈 사이에는 다양한 관계가 있는데, 그 관계를 Mock을 이용하여 끊고,
테스트하고자 하는 객체에만 집중하여 테스트하는 것이 단위 테스트이다.
빈들 간에는 다양한 관계를 맺고 있는 경우가 많습니다.
하나의 빈을 사용한다는 것은 관계된 빈들도 함께 동작한다는 것을 의미합니다.
하나의 빈을 테스트할 때 관련된 빈들이 모두 잘 동작하는지 테스트하는 것을
우리는 통합 테스트(integration test)라 합니다.
관계된 다른 클래스와는 상관 없이 특정 빈이 가지고 있는 기능만 잘 동작하는지 확인하는 것을 우리는 단위 테스트(unit test)라 합니다.
MyService
package org.edwith.webbe.calculatorcli;
import org.springframework.stereotype.Service;
@Service
public class MyService {
private final CalculatorService calculatorService;
public MyService(CalculatorService calculatorService) {
this.calculatorService = calculatorService;
}
public int execute(int value1, int value2){
return calculatorService.plus(value1, value2) * 2;
}
}
MyServiceTest
package org.edwith.webbe.calculatorcli;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {ApplicationConfig.class})
public class MyServiceTest {
@Autowired
MyService myService;
@Test
public void execute() throws Exception{
// given
int value1 = 5;
int value2 = 10;
// when
int result = myService.execute(value1, value2);
// then
Assert.assertEquals(30, result);
}
}
@RunWith(SpringJUnit4ClassRunner.class) 는 내부적으로 스프링 빈 컨테이너를 생성한다고 하였습니다. 스프링 빈 컨테이너는 빈들을 찾아 메모리에 올리게 됩니다. 그리고 나서 myService필드에 객체를 주입하게 됩니다.
테스트 메소드는 myService.execute()를 실행합니다. 5와 10을 더한 값에 2를 곱한 결과를 exeucte()메소드는 반환합니다.
assertEquals()메소드로 비교한 결과, 결과는 맞게 나올 거라고 예상할 수 있습니다. 그런데, CalculatorService의 plus()메소드에 버그가 있다면 어떻게 될까요? plus()메소드가 더하는 것이 아니라 곱한 결과를 반환한다면 어떻게 될까요?
아마 내가 생각한 값이 나오지 않을 것입니다.
MyService를 테스트하려고 하였지만 CalculatorService의 버그로 인해 에러가 날 수도 있는 것입니다. 이런 문제를 해결하려면 어떻게 해야할까요? 이런 문제를 해결하는 방법 중 하나는 목(Mock)객체를 이용하는 방법이 있습니다.
MyService가 사용하던 CalculatorService를 사용하는 대신, 가짜 객체를 하나 생성하도록 하는 것입니다. 내가 원하는 동작을 하는 Mock객체로 CalculatorService를 사용함으로써 MyService의 내용만 테스트를 수행할 수 있습니다.
이를 위해 pom.xml파일에 다음을 추가합니다.
<!-- test mock을 위한 라이브러리를 추가합니다. -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>1.9.5</version>
<scope>test</scope>
</dependency>
MyServiceTest.java
package org.edwith.webbe.calculatorcli;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.verify;
@RunWith(MockitoJUnitRunner.class)
public class MyServiceTest {
@InjectMocks
MyService myService;
@Mock
CalculatorService calculatorService;
@Test
public void execute() throws Exception{
// given
int value1 = 5;
int value2 = 10;
given(calculatorService.plus(5, 10)).willReturn(15);
//when
// when
int result = myService.execute(value1, value2);
// then
verify(calculatorService).plus(anyInt(), anyInt());
Assert.assertEquals(30, result);
}
}
@RunWith(MockitoJUnitRunner.class)
mockito가 제공하는 Junit 확장 클래스 MockitoJunitRunner를 이용해 테스트 클래스를 실행하도록 합니다.
@Mock CalculatorService calculatorService;
@Mock 어노테이션은 calculatorService가 목 객체를 참조하도록 합니다. 즉, 여러분이 객체를 생성하지 않아도 자동으로 객체가 생성되고 해당 필드가 초기화 된다는 것을 의미합니다.
@InjectMocks MyService myService;
@InjectMocks 어노테이션이 붙은 필드는 목 객체를 사용하는 MyService 객체를 생성하여 초기화하라는 의미를 가집니다. myService 역시 여러분들이 초기화하지 않아도 자동으로 MyService 객체가 생성되어 초기화 됩니다.
given(calculatorService.plus(5, 10)).willReturn(15);
given()은 static메소드입니다. import문을 보면 org.mockito.BDDMockito 클래스의 static 메소드인것을 알 수 있습니다. calculatorService는 가짜 객체입니다. 이 가짜 객체가 동작하는 방법을 규정할 수 있는 것이 given()메소드입니다.calculatorService.plus(5,10)을 호출하면 plus메소드가 15를 반환하도록 하라는 의미를 가집니다.
int result = myService.execute(value1, value2);
execute()메소드는 내부적으로 calculatorService의 plus메소드를 호출합니다.
해당 메소드는 위에서 설정한대로만 동작합니다. value1과 value2가 무슨 값이든지 간에 result는 30을 반환할 것입니다.
verify(calculatorService).plus(anyInt(), anyInt());
verify()메소드는 org.mockito.Mockito의 static한 메소드입니다. anyInt()메소드는 org.mockito.Matchers의 static한 메소드입니다. verify메소드는 파라미터로 들어온 객체의 plus메소드가 호출된 적이 있는지 검증합니다. 좀 더 자세히 살펴보자면 plus(anyInt(), anyInt())는 어떤 정수든지 2개를 파라미터로 넣어서 plus()메소드를 호출했는지를 검증합니다. myService.execute()메소드 내부적으로 plus 메소드를 호출했다면 해당 메소드는 검증을 성공한 것입니다.
만약, execute()메소드에서 plus(anyInt(), anyInt())를 호출하지 않았다면 오류가 발생하게 됩니다.