Skip to content

Commit

Permalink
Download test containers homework
Browse files Browse the repository at this point in the history
  • Loading branch information
JackKaif committed Feb 23, 2024
1 parent 673438f commit 1e9fe88
Show file tree
Hide file tree
Showing 19 changed files with 471 additions and 0 deletions.
2 changes: 2 additions & 0 deletions java-advanced-ru/test-containers/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.gradle/
build/
5 changes: 5 additions & 0 deletions java-advanced-ru/test-containers/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
test:
gradle test

start:
gradle bootRun
75 changes: 75 additions & 0 deletions java-advanced-ru/test-containers/README.html

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions java-advanced-ru/test-containers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Test container

В прошлых домашних заданиях при тестировании мы использовали встроенную базу данных H2 в памяти. Это значительно упрощало тестирование приложения. Но в некоторых случаях мы можем захотеть проверить работу приложения с реальной базой данных, с той, которая будет использоваться при работе. В этом домашнем задании вам предстоит написать тесты для проверки работы приложения с базой данных PostgreSQL. Для этого мы будем использовать библиотеку Testcontainer. Она позволит нам в тестах запустить Docker контейнер с базой данных PostgreSQL. Для выполнения этого задания вам понадобится установленный Docker.

## Ссылки

* [Интеграция JUnit и Testcontainer](https://www.testcontainers.org/test_framework_integration/junit_5/)
* [Аннотация `@Testcontainers` – Активизирует автоматический старт и остановку контейнеров в отмеченном тестовом классе](https://javadoc.io/doc/org.testcontainers/junit-jupiter/latest/org/testcontainers/junit/jupiter/Testcontainers.html)
* [Аннотация `@Container` – Отмечает контейнер, который будет запущен в тестах](https://javadoc.io/doc/org.testcontainers/junit-jupiter/latest/org/testcontainers/junit/jupiter/Container.html)
* [Аннотация `@DynamicPropertySource` – позволяет динамически установить свойства приложения при интеграционных тестах](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/test/context/DynamicPropertySource.html)
* [Класс PostgreSQLContainer – служит для создания контейнера с базой данных PostgreSQL](https://javadoc.io/static/org.testcontainers/postgresql/1.9.1/org/testcontainers/containers/PostgreSQLContainer.html)

## build.gradle

## Задачи

* Изучите код в файле *build.gradle*. Посмотрите, какие зависимости необходимы для работы библиотеки Testcontainer

## src/main/java/exercise

В этой директории находится простое Spring Boot приложение, реализующее полный CRUD сущности.

## Задачи

* Изучите код приложения. Обратите внимание, что оно использует базу данных PostgreSQL.

## src/test/java/exercise/AppTest.java

Так как наше приложение использует базу данных PostgreSQL, будем тестировать его работу именно с этой базой. Один из тестов на создание пользователей уже написан в классе, чтобы вы могли на него ориентироваться.

## Задачи

* Отметьте тестовый класс необходимой аннотацией, чтобы обеспечить автоматический старт и остановку контейнеров.

* Сделайте так, чтобы каждый тест запускался в транзакции. Использовать транзакции при тестировании базы данных является хорошей практикой. Это делает тесты независимыми друг от друга.

* Создайте контейнер для базы данных PostgreSQL. Укажите имя базы данных, имя пользователя, пароль. Для наполнения базы данных тестовыми данными используйте скрипт *src/test/resources/init.sql*

* Дальше нам потребуется указать приложению, какой источник данных использовать. Так как база данных поднимается в контейнере, мы не можем заранее знать URL базы и использовать файл *properties.yml* для указания источника. В этом случае нам потребуется установить свойства приложения динамически. Создайте публичный статический метод, который будет устанавливать свойства приложения. Метод должен принимать на вход экземпляр класса `DynamicPropertyRegistry`. Отметьте его аннотацией `@DynamicPropertySource`. Метод должен устанавливать такие свойства, как url, имя пользователя и пароль для подключения к базе данных. Пример можно посмотреть в документации по этой аннотации.

На этом создание контейнера окончено. При запуске тестов будет автоматически запущен контейнер, создастся база данных и наполнится тестовыми данными.

* Допишите тесты, которые проверяют вывод списка всех пользователей, просмотр конкретного пользователя, редактирование и удаление пользователя. Проверьте только позитивные случаи.

* Запустите тесты и убедитесь, что они отработали корректно

## Подсказки

* Изучите пример в директории *examples*
35 changes: 35 additions & 0 deletions java-advanced-ru/test-containers/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
plugins {
id 'org.springframework.boot' version '3.0.6'
id 'io.spring.dependency-management' version '1.1.0'
id 'java'
id 'com.adarshr.test-logger' version '3.0.0'
}

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'org.postgresql:postgresql'
implementation 'org.liquibase:liquibase-core'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'net.java.dev.jna:jna:5.13.0'

// Эти зависимости необходимы для работы Testcontainer
testImplementation 'org.testcontainers:junit-jupiter:1.18.3'
testImplementation 'org.testcontainers:testcontainers:1.18.3'
testImplementation 'org.testcontainers:postgresql:1.18.3'
}

test {
useJUnitPlatform()
}

testlogger {
showStandardStreams = true
}
66 changes: 66 additions & 0 deletions java-advanced-ru/test-containers/examples/Example.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
@SpringBootTest
@AutoConfigureMockMvc

// Аннотация позволяет автоматически запускать и останавливать в тестах все контейнеры
@Testcontainers
// Все тесты выполняем в транзакции
@Transactional
public class AppTest {

@Autowired
private MockMvc mockMvc;

// Аннотация отмечает контейнер, который будет автоматически запущен
@Container
// Создаём контейнер с СУБД PostgreSQL
// В конструктор передаём имя образа, который будет скачан с Dockerhub
// Если не указать версию, будет скачана последняя версия образа
private static PostgreSQLContainer<?> database = new PostgreSQLContainer<>("postgres")
// Создаём базу данных с указанным именем
.withDatabaseName("dbname")
// Указываем имя пользователя и пароль
.withUsername("sa")
.withPassword("sa")
// Скрипт, который будет выполнен при запуске контейнера и наполнит базу тестовыми данными
.withInitScript("script.sql");

// Так как мы не можем знать заранее, какой URL будет у базы данных в контейнере
// Нам потребуется установить это свойство динамически
@DynamicPropertySource
public static void properties(DynamicPropertyRegistry registry) {
// Устанавливаем URL базы данных
registry.add("spring.datasource.url", database::getJdbcUrl);
// Имя пользователя и пароль для подключения
registry.add("spring.datasource.username", database::getUsername);
registry.add("spring.datasource.password", database::getPassword);
// Эти значения приложение будет использовать при подключении к базе данных
}

// Тестируем приложение
@Test
void testCreatePerson() throws Exception {
// Добавляем нового пользователя
MockHttpServletResponse responsePost = mockMvc
.perform(
post("/people")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"firstName\": \"Jackson\", \"lastName\": \"Bind\"}")
)
.andReturn()
.getResponse();

assertThat(responsePost.getStatus()).isEqualTo(200);

// И проверяем, что пользователь добавился в базу
MockHttpServletResponse response = mockMvc
.perform(get("/people"))
.andReturn()
.getResponse();

assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentType()).isEqualTo(MediaType.APPLICATION_JSON.toString());
assertThat(response.getContentAsString()).contains("Jackson", "Bind");
}

// Остальные тесты
}
Binary file added java-advanced-ru/test-containers/presentation.pdf
Binary file not shown.
1 change: 1 addition & 0 deletions java-advanced-ru/test-containers/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
rootProject.name = 'test-container'
13 changes: 13 additions & 0 deletions java-advanced-ru/test-containers/src/main/java/exercise/App.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package exercise;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {

public static void main(String[] args) {
// Запускаем приложение
SpringApplication.run(App.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package exercise.controller;

import exercise.model.Person;
import exercise.repository.PersonRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PatchMapping;

@RestController
@RequestMapping("/people")
@RequiredArgsConstructor
public class PeopleController {

// Автоматически заполняем значение поля
private final PersonRepository personRepository;

@GetMapping(path = "")
public Iterable<Person> getPeople() {
return this.personRepository.findAll();
}

@PostMapping(path = "")
public void createPerson(@RequestBody Person person) {
this.personRepository.save(person);
}

@GetMapping(path = "/{id}")
public Person getPerson(@PathVariable long id) {
return this.personRepository.findById(id);
}

@DeleteMapping(path = "/{id}")
public void deletePerson(@PathVariable long id) {
this.personRepository.deleteById(id);
}

@PatchMapping(path = "/{id}")
public void updatePerson(@PathVariable long id, @RequestBody Person person) {
person.setId(id);
this.personRepository.save(person);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package exercise.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class WelcomeController {

@GetMapping("/")
public String root() {
return "Welcome to Spring";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package exercise.model;

import lombok.Getter;
import lombok.Setter;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Getter
@Setter
@Entity
@Table(name = "people")
public class Person {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;

private String firstName;
private String lastName;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package exercise.repository;

import exercise.model.Person;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface PersonRepository extends CrudRepository<Person, Long> {

Person findById(long id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
server:
port: ${PORT:5000}

logging:
level:
root: INFO

spring:
datasource:
url: jdbc:postgresql://localhost:5432/hexlet
username: sa
password: sa
jpa:
show-sql: true

liquibase:
change-log: classpath:db/changelog/db.changelog-master.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
databaseChangeLog:
- changeSet:
id: 1
author: hexlet
changes:
- createTable:
tableName: people
columns:
- column:
name: id
type: bigint
autoIncrement: true
constraints:
primaryKey: true
nullable: false
- column:
name: first_name
type: varchar(255)
- column:
name: last_name
type: varchar(255)
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package exercise;

import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import org.springframework.http.MediaType;

import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.junit.jupiter.Container;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.testcontainers.containers.PostgreSQLContainer;

@SpringBootTest
@AutoConfigureMockMvc

// BEGIN

// END
public class AppTest {

@Autowired
private MockMvc mockMvc;

// BEGIN

// END

@Test
void testCreatePerson() throws Exception {
MockHttpServletResponse responsePost = mockMvc
.perform(
post("/people")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"firstName\": \"Jackson\", \"lastName\": \"Bind\"}")
)
.andReturn()
.getResponse();

assertThat(responsePost.getStatus()).isEqualTo(200);

MockHttpServletResponse response = mockMvc
.perform(get("/people"))
.andReturn()
.getResponse();

assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentType()).isEqualTo(MediaType.APPLICATION_JSON.toString());
assertThat(response.getContentAsString()).contains("Jackson", "Bind");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
spring:
liquibase:
enabled: false
13 changes: 13 additions & 0 deletions java-advanced-ru/test-containers/src/test/resources/init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CREATE TABLE people (
id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
first_name varchar(255),
last_name varchar(255)
);

INSERT INTO
people (first_name, last_name)
VALUES
('John', 'Smith'),
('Jack', 'Doe'),
('Jassica', 'Simpson'),
('Robert', 'Lock');
Loading

0 comments on commit 1e9fe88

Please sign in to comment.