DEV/JUnit

[JUnit] Spring Boot 애플리케이션 테스트

Imvory 2024. 12. 10. 17:33

Spring Initializr 를 이용해 Spring 프로젝트 만들기

https://start.spring.io/

해당 페이지에서 스프링을 구동하기 위한 최소한의 설정을 가지고 스프링 부트 프로젝트 뼈대를 만들어줌

- Explore : 생성될 프로젝트에 대한 세부정보 확인 가능

- Generate : 프로젝트가 포함된 zip파일 다운로드

 

JUnit5로 테스트한 Spring 애플리케이션을 Spring Boot로 전환하기

예제 코드 : https://github.com/devkunst/junit-in-action-third-edition-kr/tree/master/ch17-spring-boot/ch17-spring-boot-initializr-old-feature

 

1. 패키지 구조 설정

- model :  모델 클래스 만들기

- registration : 승객 등록과 관련된 클래스 만들기

 

2. application-context.xml 파일의 컴포넌트 스캔 설정 제거

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="passenger" class="com.manning.junitbook.spring_boot.model.Passenger">
        <constructor-arg name="name" value="John Smith"/>
        <property name="country" ref="country"/>
        <property name="isRegistered" value="false"/>
    </bean>

    <bean id="country" class="com.manning.junitbook.spring_boot.model.Country">
        <constructor-arg name="name" value="USA"/>
        <constructor-arg name="codeName" value="US"/>
    </bean>

</beans>

 

 <context:component-scan base-package=".." /> 대신 애노테이션 사용

 

@SpringBootTest
@EnableAutoConfiguration
@ImportResource("classpath:application-context.xml")
class RegistrationTest {

	@Autowired
	private Passenger passenger;

	@Autowired
	private RegistrationManager registrationManager;

	@Test
	void testPersonRegistration() {
		registrationManager.getApplicationContext().publishEvent(new PassengerRegistrationEvent(passenger));
		System.out.println("After registering:");
		System.out.println(passenger);
		assertTrue(passenger.isRegistered());
	}

}

- @SpringBootTest : @EnableAutoConfiguration 애노테이션과 함께 현재 테스트 클래스 패키지와 서브 패키지에서 스프링 빈을 검색한다. 해당 애노테이션이 빈을 알아서 찾아 @Autowired (객체 주입)이 가능하다

- @ImportResource : XML 설정에 여전히 데이터 빈이 존재할 경우 설정파일로부터 빈을 가져오는 애노테이션

 

실행결과 (성공)

 

Spring Boot에서 테스트 전용 구성 구현하기

xml은 전통적인 스프링 빈 구성 방식으로 외부 의존성이 필요하지 않으며 소스 코드를 다시 컴파일할 필요가 없어 바꾸기 쉽다. 그러나 Spring Boot에서 제공하는 애노테이션을 사용하여 이를 대체한다면 Java 오류를 사전에 방지할 수 있다. 설정이 잘못 되었을 경우 컴파일 오류를 띄우기 떄문에, Java 기반으로 정의된 Bean은 타입에 안전하다.

 

1. application-context.xml 의 기존 Bean을 @TestConfiguration 애노테이션으로 대체하기

package com.manning.junitbook.spring_boot.beans;

import com.manning.junitbook.spring_boot.model.Country;
import com.manning.junitbook.spring_boot.model.Passenger;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;

@TestConfiguration
public class TestBeans {

    @Bean
    Passenger createPassenger() {
        Passenger passenger = new Passenger("John Smith");
        passenger.setCountry(createCountry());
        passenger.setIsRegistered(false);
        return passenger;
    }

    @Bean
    Country createCountry() {
        Country country = new Country("USA", "US");
        return country;
    }
}

- @TestConfiguration : 빈을 정의하거나 테스트에 적용하기 위한 사용자 정의 구성에 사용할 수 있다.

- @Bean 애노테이션을 통해 해당 빈을 사용할 것을 명시

- RegistrationManager와 PassengerRegistrationListener 같은 기능 빈은 @Service로 등록한다.

- 기존의 applicaition-context.xml 은 삭제

 

2. RegistrationTest 클래스 수정

@SpringBootTest
@Import(TestBeans.class)
class RegistrationTest {
...
}

- @EnableAutoConfiguration, @ImportResource로 빈을 가져오던 방식을 @Import 애노테이션으로 변경

 

실행결과 (동일하게 성공)

 

신규 기능 추가하여 Spring Boot 애플리케이션 테스트하기

: 항공편 생성 및 설정, 항공편에 승객 추가 및 삭제 기능 추가

 

1.  Flight 클래스 생성

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

public class Flight {

    private String flightNumber;
    private int seats;
    private Set<Passenger> passengers = new HashSet<>();

    public Flight(String flightNumber, int seats) {
        this.flightNumber = flightNumber;
        this.seats = seats;
    }

    public String getFlightNumber() {
        return flightNumber;
    }

    public int getSeats() {
        return seats;
    }

    public Set<Passenger> getPassengers() {
        return Collections.unmodifiableSet(passengers);
    }

    public boolean addPassenger(Passenger passenger) {
        if (passengers.size() >= seats) {
            throw new RuntimeException("항공편의 좌석 수보다 더 많은 승객을 추가할 수 없습니다");
        }
        return passengers.add(passenger);
    }

    public boolean removePassenger(Passenger passenger) {
        return passengers.remove(passenger);
    }

    @Override
    public String toString() {
        return "Flight " + getFlightNumber();
    }

}

- getPassenger() : unmodifiableSet을 사용하여 수정할 수 없는 승객 정보 리스트를 반환

 

2. 승객 리스트 CSV 생성

flights_infromation.csv

John Smith; UK
Jane Underwood; AU
James Perkins; US
Mary Calderon; US
Noah Graves; UK
Jake Chavez; AU
Oliver Aguilar; US
Emma McCann; AU
Margaret Knight; US
Amelia Curry; UK
Jack Vaughn; US
Liam Lewis; AU
Olivia Reyes; US
Samantha Poole; AU
Patricia Jordan; UK
Robert Sherman; US
Mason Burton; AU
Harry Christensen; UK
Jennifer Mills; US
Sophia Graham; UK

 

3. FlightBuilder 클래스 생성

: CSV 파일을 읽어와 항공편에 승객 정보를 채워 넣음

import com.manning.junitbook.spring_boot.model.Country;
import com.manning.junitbook.spring_boot.model.Flight;
import com.manning.junitbook.spring_boot.model.Passenger;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@TestConfiguration
public class FlightBuilder {

    private static Map<String, Country> countriesMap = new HashMap<>();

    static {
        countriesMap.put("AU", new Country("Australia", "AU"));
        countriesMap.put("US", new Country("USA", "US"));
        countriesMap.put("UK", new Country("United Kingdom", "UK"));
    }

    @Bean
    Flight buildFlightFromCsv() throws IOException {
        Flight flight = new Flight("AA1234", 20);
        try (BufferedReader reader = new BufferedReader(new FileReader("src/test/resources/flights_information.csv"))) {
            String line = null;
            do {
                line = reader.readLine();
                if (line != null) {
                    String[] passengerString = line.toString().split(";");
                    Passenger passenger = new Passenger(passengerString[0].trim());
                    passenger.setCountry(countriesMap.get(passengerString[1].trim()));
                    passenger.setIsRegistered(false); // 승객을 미등록 상태로 설정
                    flight.addPassenger(passenger); // 항공편에 승객 추가
                }
            } while (line != null);

        }

        return flight;
    }
}

 

4. FlightTest 클래스 생성

: 항공편에 추가된 모든 승객을 등록하고 등록이 완료되었는지 테스트 검증

import com.manning.junitbook.spring_boot.beans.FlightBuilder;
import com.manning.junitbook.spring_boot.model.Flight;
import com.manning.junitbook.spring_boot.model.Passenger;
import com.manning.junitbook.spring_boot.registration.PassengerRegistrationEvent;
import com.manning.junitbook.spring_boot.registration.RegistrationManager;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

@SpringBootTest
@Import(FlightBuilder.class)
public class FlightTest {

    @Autowired
    private Flight flight;

    @Autowired
    private RegistrationManager registrationManager;

    @Test
    void testFlightPassengersRegistration() {
        for (Passenger passenger : flight.getPassengers()) {
            assertFalse(passenger.isRegistered());
            registrationManager.getApplicationContext().publishEvent(new PassengerRegistrationEvent(passenger));
        }

        System.out.println("모든 승객이 등록되었는지 확인");

        for (Passenger passenger : flight.getPassengers()) {
            assertTrue(passenger.isRegistered());
        }
    }
}

 

실행결과