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/
Leave a comment