1_[spring]junit5 맛보기 java with gradle!
JUnit이란?
00. TDD 개발방식
- Test-driven Development
- 테스트 주도 개발에서 사용
- 코드의 유지보수 및 운영환경에서의 에러를 미리 방지하기 위해서 단위 별로 검증하는 테스트 프레임워크
📍 단위 테스트 📍
- 작성한 코드가 기대하는 대로 동작을 하는지 검증하는 절차
- 언어별로 단위테스트를 지원하는 프레임워크가 존재
01. JUnit
- 자바 기반의 단위테스트를 위한 프레임워크
- 어노테이션 기반으로 테스트 지원 ▶️ 테스트가 용이
- Assert(
예상값,실제값)를 통해 검증
02. Let’s practice JUnit
먼저 JUnit 방식에 익숙해지기 위해서 java gradle 프로젝트를 시작하자
그러면 아레처럼 주피터 엔진과 junit , useJUnitPlatform()이 있어야 한다!
plugins {
id 'java'
}
group 'org.example'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
**testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'**
**testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'**
}
test {
useJUnitPlatform()
}
계산기를 만들어보면서 익혀보자
먼저 더하기와 빼기를 하는 계산기 인터페이스 ICalculator를 만들어보자
public interface ICalculator {
int sum(int x, int y);
int minus(int x, int y);
}
그리고 이를 구현하지 않고, 멤버변수로 두고 사용하는 클래스 Calculator를 만들어보자
public class Calculator{
//구현하지 않고
//포함시켜서 진행
private ICalculator iCalculator;
public Calculator(ICalculator iCalculator){
this.iCalculator=iCalculator;
}
public int sum(int x, int y){
return this.iCalculator.sum(x,y);
}
public int minus(int x, int y){
return this.iCalculator.minus(x,y);
}
}
그리고 Calculator의 생성자에 보면, 구현체를 넣어주어야 하는데 구현체가 없기 때문에, ICalculator를 구현한 구현체 KrwCalculator를 만들어보자
public class KrwCalculator implements ICalculator {
private int price=1;
@Override
public int sum(int x, int y) {
x*=price;
y*=price;
return x+y;
}
@Override
public int minus(int x, int y) {
x*=price;
y*=price;
return x-y;
}
}
이를 이용해서 KrwCalculator를 Calculator에 전달해주고, 이를 기반으로 sum메서드를 진행하는데, 그 인자값으로 x와 y값으로 10을 넣어주자
public class Main {
public static void main(String[] args){
System.out.println("hello JUnit");
Calculator c=new Calculator(new KrwCalculator());
System.out.println(c.sum(10,10));
}
}
그러면 아래처럼 더한 결과가 잘 나오는 것을 확인해볼 수 있다
hello JUnit
20
그리고 이번에는 “해외 시세”에 따른 금액 변동이 있는 Dollar의 경우를 생각해보자
이 경우에는 시세를 “외부”에서 가져와야 하기 때문에 일종의 ‘통신’이 필요하다
그런데 이는 계산기 목적과 상이하기 때문에, 시세를 의미하는 MarketApi로 분리하고 간단하게 1100원이 시세라고 가정하자
public class MarketApi {
public int connect(){
return 1100;
}
}
이를 이용해서 DollarCalculator에서는 생성자를 이용해서 MarketApi를 주입해주고, price에 이 값을 넣어 초기화해주자
public class DollarCalculator implements ICalculator {
private int price =1;
//통신을 통해서 시세를 가져와서 적용하기 위함
//그런데 계산기의 목적과 "통신"은 부적절하기 때문에 분리할것
private MarketApi marketApi;
public DollarCalculator(MarketApi marketApi){
this.marketApi=marketApi;
}
public void init(){
this.price= marketApi.connect();
}
@Override
public int sum(int x, int y) {
x*=price;
y*=price;
return x+y;
}
@Override
public int minus(int x, int y) {
x*=price;
y*=price;
return x-y;
}
}
그러면 이를 메인에서 똑같이 x와 y에 값을 10으로 넣으면 이제는 MarketApi로 인해서 시세가 적용되어, 22000원이 계산되었음을 확인해볼 수 있다
public class Main {
public static void main(String[] args){
System.out.println("hello JUnit");
// Calculator c=new Calculator(new KrwCalculator());
//
// System.out.println(c.sum(10,10));
MarketApi marketApi=new MarketApi();
DollarCalculator d= new DollarCalculator(marketApi);
d.init();
Calculator c2=new Calculator(d);
System.out.println(c2.sum(10,10));
}
}
hello JUnit
22000
그런데, 만약 DollarCalculator에서 sum메서드가 return 0이라면 언제까지 ctrl을 누르고 이 원인을 파악하러 몇 개의 클래스를 건널 것인가?
▶️ test 폴더를 이용하자!! 🌟🌟🌟
✴️ 잠깐! 테스트 중에 이런 표시를 보셨다구요? 겁내지 마십시오!
0 containers and 1 tests were Method or class mismatch
가장 빠른 방법은 ‘무시’입니다!(허허)
https://velog.io/@wiswis3434/Day-01-오류모음
원인은 “메서드가 여러개인데 1개만 테스트했기 때문”이라고 합니다
https://www.inflearn.com/questions/157200
하지만, 그래도 테스트가 문제가 있다(진행에)
- Settings-Build,Execution,Deployment-Build Tools-Gradle에서
Build and run using과Run tests using속성을 Intellij IDEA로 변경[https://www.inflearn.com/questions/157200] - https://intellij-support.jetbrains.com/hc/en-us/community/posts/360004383639-How-to-add-Gradle-options 를 참고해서 Run-Edit Configurations-Environment variables 수정
나는 문구가 떴지만, 그냥 테스트 결과가 확인되어서 문제가 없었다!
다시 컴백 투 본론!
삽질 끝에 문제가 없는 것 같아서 쭉 진행하기로 했다
위의 DollarController에서 sum과 minus에서 리턴되는 값을 0으로 해두자(일부러)
그리고 단위테스트를 하기 위해서는
- Test 어노테이션
- Assertions.~ 메서드를 통해서 값 비교를 해주어야 한다!(배열관련 비교 메서드도 있어서 ~로 적었다)
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class DollarCalculatorTest {
@Test
public void testHello(){
System.out.println("hello");
}
📌 **@Test**
public void dollarTest(){
MarketApi marketApi=new MarketApi();
DollarCalculator d= new DollarCalculator(marketApi);
d.init();
Calculator c2=new Calculator(d);
System.out.println(c2.sum(10,10));
📌 **Assertions.assertEquals(22000,c2.sum(10,10));**
}
}

그리고 dollarTest() 메서드에 대해서 왼쪽에 뜨는 실행 버튼을 눌러주면 예측값이 22000인데, 실제로 테스트해봤을 때 0이 나왔고, 관련된 부분을 같이 띄워준다
그러면 우리는 ‘엇! 그러면 여기서 잘못되었던 거구나!’ 라고 생각해서 해당 부분을 수정해주면 될 것이다
그런데, 조금만 더 상황을 가정해서 생각해보자
지금 sum메서드와 minus 메서드의 차이점은 +를 할지 -를 할 지 이지 않은가?
그런데 만약, 돈이 걸려있었던 플젝인데 복붙만 하고 수정을 하지 않았다면..?
큰 위기가 있을 것인데, 이를 이전에 방지하는데에 이러한 테스트코드가 위대한 역할을 해줄 수 있을 것이다! 뿐만 아니라, 다른 팀원이 수정 후 코드 확인을 할 때에도 도움이 될 수 있을 것이다!
03. Mockito
- 가짜(Mock) 객체의 의존성 주입을 해서 예상하는 값을 반환하도록 상황을 모사해주는 것
- 예) User라는 객체가 있는데, 이를 사용중이라면 Mock 객체를 만들어 테스트
- Mockito core와 Mockito JUnit Jupiter 에 대한 의존성을 추가해주어야 한다
- test 클래스 위에
@ExtendWith(MockitoExtension.class)를 추가해주어야 한다!
plugins {
id 'java'
}
group 'org.example'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
**// https://mvnrepository.com/artifact/org.mockito/mockito-core
testImplementation group: 'org.mockito', name: 'mockito-core', version: '3.6.0'
// https://mvnrepository.com/artifact/org.mockito/mockito-junit-jupiter
testImplementation group: 'org.mockito', name: 'mockito-junit-jupiter', version: '3.6.0'**
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0
test {
useJUnitPlatform()
}
위: build.gradle
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
**@ExtendWith(MockitoExtension.class)**
public class DollarCalculatorTest {
@Test
public void testHello(){
System.out.println("hello");
}
@Test
public void dollarTest(){
MarketApi marketApi=new MarketApi();
DollarCalculator d= new DollarCalculator(marketApi);
d.init();
Calculator c2=new Calculator(d);
System.out.println(c2.sum(10,10));
Assertions.assertEquals(22000,c2.sum(10,10));
}
}
MarketApi를 가짜 객체(Mock Object)로 만들어 사용해보자!
🌹 Mockito.lenient(), MockSettings.lenient()
- Mockito 2부터 도입
- 더 편리한 테스트와 향상된 생산성을 유도
- 엄격한 stubbing은 불필요한 stub들을 보고함으로써 테스트를 조금더 dry하게 만드는데, lenient 는 이에 대한 절충안을 제시
(외계어인 것인가.. 어려워😭)
03-1. TDD의 원초적 개념부터 접근, What is Mocking / Stubbing?
아래의 해외 블로그를 참고해서 TDD 부분을 정리해보았다😢
How to test software, part I: mocking, stubbing, and contract testing

위의 삼각형은 사이트에서 “테스트 피라미드”로 소개하고 있다
- 가장 빠르고 저렴한 방법으로 테스트할 수 있는 것은 단위/요소 테스트
- 통합 테스트는 이러한 단위테스트보다는 조금 더 느리고 기회비용이 비싸다
- 그리고 UI 계층에서 테스트 하는것이 이 세가지 중 가장 느리고 기회비용이 가장 비싸다
🌻 What is Mocking ? 🌻
실제 서비스를 대신할 수 있는 가짜 버전의 외부/내부 서비스를 만들어 테스트를 보다 빠르고 안정적으로 실행하는 것
- 객체의 속성과 상호작용할 때 모의 객체를 사용할 수 있음(객체의 기능이나 동작과 관련된 작용이 아니라!)
🌻 What is Stubbing ? 🌻
Mocking처럼 대리인(stand-in) 을 만들지만, 전체 객체가 아닌, 행위(행동;behavior)를 모의 테스트 하는 것
궁금한 것은 둘의 차이가 행위인지 속성인지라는 점에서 차이가 있다는 점이다! 그래서 mock과 stub의 차이라던지 관련 개념을 설명해둔 곳이 필요했다
그 결과 멋진! 블로그를 발견했다!(감사합니다!!)
https://joont92.github.io/tdd/상태검증과-행위검증-stub과-mock-차이/
https://minslovey.tistory.com/97
https://testing.jabberstory.net/
이 블로그를 기반으로 Mock과 Stub, 검증 등에 대해서 추가로 정리해보면서 공부해보도록 할 것이다
03-1-1. 관련 용어 정리
🌹 SUT ( System Under Test) : 주요 객체(primary object)! 테스트 대상
🌹 협력객체(collaborator) : 부차적 객체(secondary objects)
🌹 테스트 더블(Test Double) : 테스팅을 목적으로 진짜 객체 대신 사용되는 모든 종류의 위장 객체
- Dummy, Fake Object, Stub, Mock
- 관련해서는 나중에 더 공부해보자(아래)
What’s the difference between faking, mocking, and stubbing?
03-1-2. 상태검증과 행위검증의 차이
🌹 상태검증 : 메서드가 수행된 후 주요객체(SUT) 및 협력객체(collaborator)의 상태를 살펴봄으로써 올바르게 동작했는지 판단
🌹 행위검증 : 주요객체(SUT)가 협력객체(collaborator)의 특정 메서드가 호출되었는지 등의 행위를 검사함으로써 올바르게 동작했는지 판단
03-1-3. stub vs mock
🌹 stub
- 호출이 되면,
미리 준비된 답변으로 응답하는 것 - 테스트 시,
프로그램된 것 이외에는 응답 🚫 - 협력 객체의 특정 부분이 테스트하기 어려울 경우, stub를 사용하면 수월
- 일반적으로 우리가 mock으로 잘못 알고 있음
🌹 mock
- 다른 테스트 더블과는 다르게,
행위검증 사용을 추구 - 행위를 기록하는 식의 로직이 들어가 있을 것
03-2. Mockito를 이용해서 주요 객체 SUT의 행위를 검증하기
먼저, 우리는 위의 예제를 이어서 진행해볼 예정이다
이때, 협력객체가 MarketApi 이다!
그리고 lanient를 이용해서 조금 느슨하게 connect 메서드가 실행되었을 때(when) 3000을 리턴하도록 해주자(mocking 처리)(thenReturn)
@BeforeEach
- 각각의 test 메서드 실행 전에 노출되어 처리
@AfterEach
- 각각의 test 메서드 실행 후에 노출되어 처리
@BeforeAll
- 클래스에 존재하는 모든 메서드 시작 전 실행
@AfterAll
- 클래스에 존재하는 모든 메서드 시작 후 실행
우리는 협력객체인 MarketApi를 필드로 만들어 두었으므로, mocking을 확인해볼 메서드인 mockTest() 내부에서는 새로이 객체를 만들 필요가 없다
그리고, 협력객체는 주력 객체의 실행 전에 init 메서드를 실행한 상황이어야 하므로 @BeforeEach 를 이용해서 먼저 모의 객체를 만들어서 값을 주입해주자
🌟 Mocking 작업 대상이 될 mock 객체인 MarketApi에 대해서는 @Mock 어노테이션을 붙이자
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
**@ExtendWith(MockitoExtension.class)**
public class DollarCalculatorTest {
**@Mock
public MarketApi marketApi;**
**@BeforeEach**
public void init(){
**Mockito.lenient().when(marketApi.connect()).thenReturn(3000);**
}
@Test
public void testHello(){
System.out.println("hello");
}
@Test
public void dollarTest(){
MarketApi marketApi=new MarketApi();
DollarCalculator d= new DollarCalculator(marketApi);
d.init();
Calculator c2=new Calculator(d);
System.out.println(c2.sum(10,10));
Assertions.assertEquals(22000,c2.sum(10,10));
}
**@Test**
public void mockTest(){
// MarketApi marketApi=new MarketApi();
DollarCalculator d= new DollarCalculator(marketApi);
d.init();
Calculator c2=new Calculator(d);
System.out.println(c2.sum(10,10));
Assertions.assertEquals(60000,c2.sum(10,10));
}
}
그러면 아래와 같이 우리가 지금 위에서는 60000으로 예측했는데, 처음 테스트시에는 22000으로 예측했었다.
이럴 경우에는 테스트에 실패했음을 통보받고, 예측값과 실제값을 확인해볼 수 있고
중요한 것은 이제 더이상 init으로 1100원이 시세가 아니라 , 위에서 지정했던 3000이 시세가 되었음이라는 점이다!

그리고 테스트 결과를 아래처럼 확인해봄으로써 우리가 실행한 테스트가 모두 통과되었는지 점검함으로써 발생가능한 위기상황을 어느정도 잡아줄 수 있다