Testy integracyjne w Spring Boot

Testy integracyjne w Spring Boot

Testy jednostkowe koncentrują się na jednostkach kodu, zwykle pojedynczych metodach lub wierszach kodu. Testują wyłącznie lokalne wykonywanie metod i jest niewskazane aby testowane metody odnosiły się do zewnętrznych, zdalnych zasobów. Testy integracyjne natomiast to rodzaj testów w którym badana jest prawidłowość działania oprogramowania w kontekście interakcji między modułami. W tym wpisie napiszemy prostą aplikację w architekturze REST oraz przetestujemy ją integracyjnie. Do dzieła! Tworzymy nowy projekt Spring Boota – plik pom.xml:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
 
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
 
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
    </dependency>
 
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

Dla uproszczenia w projekcie wykorzystana zostanie baza plikowa H2. Konfigurujemy bazę H2 w pliku application.properties:

spring.h2.console.enabled = true
spring.h2.console.path    = /h2
 
spring.datasource.url             = jdbc:h2:mem:testdb
spring.datasource.driverClassName = org.h2.Driver
spring.datasource.username        = sa
spring.datasource.password        =
spring.jpa.database-platform      = org.hibernate.dialect.H2Dialect

Aplikacja startuje na domyślnym porcie 8080, pod adresem:

http://localhost:8080/h2

otrzymamy dostęp do konsoli (panelu administracyjnego) bazy H2.

Projektujemy Rest Api:

@RestController
public class CourseController {
 
    private CourseService courseService;
 
    public CourseController(CourseService courseService) {
        this.courseService = courseService;
    }
 
    @PostMapping("/courses")
    public ResponseEntity<Void> createCourse(@RequestBody Course course) {
        URI location = ServletUriComponentsBuilder.fromCurrentRequest()
                .path("/{id}")
                .buildAndExpand(courseService.saveCourse(course))
                .toUri();
 
        return ResponseEntity.created(location).build();
    }
 
    @GetMapping("/courses/{courseId}")
    public Course getCourseById(@PathVariable Integer courseId) {
        return courseService.getCourseByCourseId(courseId);
    }
 
}

Encja modelu (oczywiście można również dodać do projektu Lomboka):

@Entity
public class Course {
 
    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    int id;
 
    String name;
 
    public Course() {
    }
 
    public Course(String name) {
        this.name = name;
    }
 
    public int getId() {
        return id;
    }
 
    public void setId(int id) {
        this.id = id;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
}

Klasa serwisu – zapis nowego kursu, pobieranie kursu po id:

@Component
public class CourseService {
 
    private CourseRepository repository;
 
    public CourseService(CourseRepository repository) {
        this.repository = repository;
    }
 
    public int saveCourse(Course course) {
        Course savedCourse = repository.save(course);
        return savedCourse.getId();
    }
 
    public Course getCourseByCourseId(Integer courseId) {
       return repository.findById(courseId).orElse(new Course());
    }
}

Repozytorium:

@Repository
public interface CourseRepository extends CrudRepository<Course, Integer> {
}

Zapis nowego kursu:

Metoda POST -> http://localhost:8080/courses/

dane:

{
  "name": "Tom"
}

Odczyt danego kursu:

Metoda GET -> http://localhost:8080/courses/1

Piszemy testy integracyjne!

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CourseControllerTests {
 
    @LocalServerPort
    private int port;
 
    TestRestTemplate restTemplate = new TestRestTemplate();
    HttpHeaders headers           = new HttpHeaders();
 
    @Test
    public void testCreateStudent() throws Exception {
        HttpEntity<Course> entity       = new HttpEntity<Course>(new Course("Tom"), headers);
        ResponseEntity<String> response = restTemplate.exchange(createURLWithPort("/courses"), HttpMethod.POST, entity, String.class);
        assertTrue(response.getHeaders().get(HttpHeaders.LOCATION).get(0).contains("/courses"));
    }    
 
    @Test
    public void testRetrieveStudent() throws Exception {
        HttpEntity<String> entity = new HttpEntity<String>(null, headers);
        ResponseEntity<String> response = restTemplate.exchange(createURLWithPort("/courses/1"), HttpMethod.GET, entity, String.class);
        JSONAssert.assertEquals(createSampleJsonData(), response.getBody(), false);
    }
 
    private String createURLWithPort(String uri) {
        return "http://localhost:" + port + uri;
    }
 
    private String createSampleJsonData() {
        return "{\"id\":1,\"name\":\"Tom\"}";
    }
 
}

Powyżej napisane są 2 testy. Pierwszy z nich weryfikuje czy udało się prawidłowo zapisać w bazie danych nowy kurs. Drugi z kolei sprawdza czy wartość zwracana po dodaniu nowego kursu jest prawidłowa. Kilka słów wyjaśnienia:

JSONAssert.assertEquals(createSampleJsonData(), response.getBody(), true);

weryfikuje czy odpowiedź jest zgodna z oczekiwanymi danymi, parametr true oznacza, że wszystkie właściwości zwrócone w odpowiedzi w formacie JSON muszą być podane a ich wartości muszą się zgadzać z tymi które są oczekiwane. Nie ma z kolei znaczenia tutaj ich kolejność.

webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT

uruchamia testy integracyjne na losowym porcie, adnotacja @SpringBootTest uruchamia pełny kontekst Springa. Po więcej informacji zapraszam do wpisu – https://javaleader.pl/2019/11/12/kontekst-aplikacji-w-springu/

Testy z użyciem atrapy bazy danych:

Kiedy baza danych jest niedostępna można użyć atrapy (Mocka) repozytorium (korzystam z JUnit w wersji 5 zatem dodaje rozszerzenie do biblioteki Mockito):

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith(MockitoExtension.class)
public class CourseControllerMockTests {
 
    @MockBean
    private CourseRepository courseRepository;
 
    @InjectMocks
    CourseService courseService;
 
    @Test
    public void testRetrieveCourseWithMockRepository() throws Exception {
        Course course = new Course("Tom");
        course.setId(1);
        when(courseRepository.findById(1)).thenReturn(Optional.of(course));
        assertTrue(courseService.getCourseByCourseId(1).getName().contains("Tom"));
    }
}

adnotacja @InjectMocks wskazuje klasę do której wstrzykiwana jest atrapa repozytorium bazy danych. Brak użycia rozszerzenia do biblioteki Mockito zakończy się klasycznym (zdecydowanie najczęściej występującym podczas programowania w Javie) błędem:

java.lang.NullPointerException

Dla danych reaktywnych należy do testów skorzystać z Klasy WebTestClient – odsyłam w tym miejscu do artykułu – https://javaleader.pl/2020/01/23/spring-webflux-programowanie-reaktywne-w-springu/

Zobacz kod na GitHubie i zapisz się na bezpłatny newsletter!

.

Leave a comment

Your email address will not be published.


*