SpringBootTest etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster
SpringBootTest etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster

22 Ocak 2023 Pazar

SpringTest Testcontainers MariaDBContainer

Gradle
Örnek
Şu satırı dahil ederiz
testImplementation "org.testcontainers:testcontainers:1.17.3"
testImplementation "org.testcontainers:mariadb:1.17.3"
application-test.yml Dosyası
Örnek
Şöyle yaparız
spring:
datasource: driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver url: jdbc:tc:mariadb:10.5:///?TC_DAEMON=true jpa: properties: hibernate: dialect: org.hibernate.dialect.MariaDB103Dialect




9 Kasım 2022 Çarşamba

SpringTest Testcontainers Kullanımı - TestContainer Sınıfından Kalıtma

Giriş
- Bu kullanım Spring'in @DynamicPropertySource anotasyonunu kullanmadığı için daha çok Spring kullanmayan ama Jupiter ile birim testi yapmak isteyen projeler için daha uygun

- TestContainer başlatıldıktan sonra url, username gibi şeyler System property olarak atanıyor. Böylece spring bunlara erişebiliyor.

Örnek
Şöyle yaparız
import org.testcontainers.containers.OracleContainer;

public class OracleTestContainer extends OracleContainer {

  public static final String DB_CONNECTION_URL = "DB_URL";
  public static final String DB_USERNAME = "DB_USERNAME";
  public static final String DB_PASSWORD = "DB_PASSWORD";
  private static final String IMAGE_VERSION = "oracleinanutshell/oracle-xe-11g:1.0.0";
  private static OracleTestContainer container;

  private OracleTestContainer() {
    super(IMAGE_VERSION);
  }

  public static OracleTestContainer getInstance(String initScriptPath) {
    if (container == null) {
      container = new OracleTestContainer();
      container.withInitScript(initScriptPath);
      container.start();
    }
    return container;
  }

  @Override
  public void start() {
    super.start();
    System.setProperty(DB_CONNECTION_URL, container.getJdbcUrl());
    System.setProperty(DB_USERNAME, container.getUsername());
    System.setProperty(DB_PASSWORD, container.getPassword());
  }
}
Testlerde kullanılacak bir SpringConfig sınıfı tanımlanır. Şöyle yaparız
@ComponentScan(basePackages = {"com.test.app"})
@EnableJpaRepositories(basePackages = "com.test.app")
@EntityScan(basePackageClasses = {RocApplicant.class, ... })
public class ApplicantTestConfig {

}
Soyut bir test sınıfı yaratılır. Şöyle yaparız
@ActiveProfiles("test")
@DataJpaTest
@ContextConfiguration(classes = {ApplicantTestConfig.class})
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Rollback(false)
@EnableAutoConfiguration
public abstract class AbstractTest {

  public static final String INIT_SCRIPT = "sql/test-container-init.sql";

  public static OracleContainer oracle = OracleTestContainer.getInstance(INIT_SCRIPT);

  @BeforeAll
  static void setUp() {
    if (!oracle.isCreated()) {
      oracle.start();
    }
  }
}
Spring'in bağlanabilmesi için application-test.yaml dosyası oluşturulur. Şöyle yaparız
spring:
  datasource:
    driverClassName: oracle.jdbc.OracleDriver
    url: ${DB_URL}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: 2
  test:
    database:
      replace: none
  main:
    allow-bean-definition-overriding: true
  jpa:
    hibernate:
      ddl-auto: none
      database-platform: org.hibernate.dialect.Oracle10gDialect
test sınıfı şöyledir. Burada ilk sql veri tabanını temizler, ikincisi ise doldurur.
class RocApplicantServiceTest extends AbstractTest {
    
  @Autowired
  RocApplicantService mnsRocApplicantService;

  @Test
  @Sql(value = {"classpath:/sql/clean_roc_applicants.sql",
                "classpath:/sql/insert_roc_applicants.sql"})
  void createApplicant() {
    var response = mnsRocApplicantService.createApplicant(...);
    assertNotNull(response);
  }
}


3 Kasım 2022 Perşembe

SpringTest Testcontainers Kafka + Zookeeper + Avro Schema Registry

Giriş
Şu satırı dahil ederiz
import com.github.dockerjava.api.model.HostConfig;
import com.github.dockerjava.api.model.PortBinding;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
Şöyle yaparız
@SpringBootTest(properties = "spring.main.web-application-type=reactive")
@Testcontainers
public class AbstractSpringTest {
  public static final int KAFKA_PORT = 19092;
  private static final Logger LOGGER = LoggerFactory.getLogger(AbstractSpringTest.class);
  private static final Network SHARED_NETWORK = Network.newNetwork();
  private static final int SCHEMA_REGISTRY_PORT = 8081;
  @Container
  static GenericContainer<?> zookeeper = zkContainer();
  @Container
  static GenericContainer<?> kafka = kafkaContainer();
  @Container
  static GenericContainer<?> schemaRegistry = schemaRegistryContainer();


  @DynamicPropertySource
  static void registerPgProperties(DynamicPropertyRegistry registry) {
    registry.add("kafka.bootstrap-servers", () -> kafka.getHost()+":"+kafka.getMappedPort(KAFKA_PORT));
    registry.add("kafka.schema.registry.url", () -> String.format("http://%s:%d",
                schemaRegistry.getHost(), schemaRegistry.getMappedPort(SCHEMA_REGISTRY_PORT)));
    registry.add("kafka.consumer.enabled",()->"true");
  }

  private static GenericContainer<?> zkContainer() {
    return new GenericContainer<>(DockerImageName.parse("confluentinc/cp-zookeeper:5.4.10-1-ubi8"))
      .withExposedPorts(2181)
      .withCreateContainerCmdModifier(cmd ->
        cmd.withName("zookeeper")
          .withHostConfig(HostConfig.newHostConfig()
                         .withNetworkMode(SHARED_NETWORK.getId())
                         .withPortBindings(PortBinding.parse("12181:2181"))
          )
        )
        .withLogConsumer(new Slf4jLogConsumer(LOGGER))
        .withNetwork(SHARED_NETWORK).withNetworkMode(SHARED_NETWORK.getId())
        .withNetworkAliases("zookeeper")
        .withEnv("ZOOKEEPER_SERVER_ID", "1")
        .withEnv("ZOOKEEPER_CLIENT_PORT", "2181")
        .withEnv("ZOOKEEPER_TICK_TIME", "2000")
        .withEnv("ZOOKEEPER_SERVERS", "zookeeper:22888:23888");
    }

  private static GenericContainer<?> kafkaContainer() {
    return new GenericContainer<>(DockerImageName.parse("confluentinc/cp-kafka:7.2.2"))
      .withCreateContainerCmdModifier(cmd ->
        cmd.withName("kafka")
           .withHostConfig(HostConfig.newHostConfig()
                                     .withNetworkMode(SHARED_NETWORK.getId())
                                     .withPortBindings(PortBinding.parse(KAFKA_PORT+":"+KAFKA_PORT))
                              )
        )
        .withNetwork(SHARED_NETWORK).withNetworkMode(SHARED_NETWORK.getId())
        .withNetworkAliases("kafka")
        .withAccessToHost(true)
        .withExposedPorts(KAFKA_PORT)
        .withEnv("KAFKA_SCHEMA_REGISTRY_URL", "schemaregistry:8081")
        .withEnv("KAFKA_ZOOKEEPER_CONNECT", "zookeeper:2181")
        .withEnv("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP", "PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT")
        .withEnv("KAFKA_INTER_BROKER_LISTENER_NAME", "PLAINTEXT")
        .withEnv("KAFKA_ADVERTISED_LISTENERS", "PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:19092")
        .withEnv("KAFKA_BROKER_ID", "1")
        .withEnv("KAFKA_BROKER_RACK", "r1")
        .withEnv("KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR", "1")
        .withEnv("KAFKA_DELETE_TOPIC_ENABLE", "true")
        .withEnv("KAFKA_AUTO_CREATE_TOPICS_ENABLE", "true")
        .withEnv("KAFKA_JMX_PORT", "9991")
        .withLogConsumer(new Slf4jLogConsumer(LOGGER))
        .dependsOn(zookeeper).withStartupTimeout(Duration.ofMinutes(2L));
  }

  private static GenericContainer<?> schemaRegistryContainer() {
    return new GenericContainer<>(DockerImageName.parse("confluentinc/cp-schema-registry:5.4.10"))
      .dependsOn(kafka)
      .withCreateContainerCmdModifier(cmd -> cmd.withName("schemaregistry")
                        .withHostConfig(HostConfig.newHostConfig()
                                .withNetworkMode(SHARED_NETWORK.getId())
                                .withPortBindings(PortBinding.parse("18081:8081"))
                        )
      )
      .withExposedPorts(SCHEMA_REGISTRY_PORT)
      .withNetwork(SHARED_NETWORK).withNetworkMode(SHARED_NETWORK.getId())
      .withNetworkAliases("schemaregistry")
      .withAccessToHost(true)
      .withEnv("SCHEMA_REGISTRY_KAFKASTORE_CONNECTION_URL", "zookeeper:2181")
      .withEnv("SCHEMA_REGISTRY_HOST_NAME", "schemaregistry")
      .withEnv("SCHEMA_REGISTRY_LISTENERS", "http://0.0.0.0:8081")
      .withLogConsumer(new Slf4jLogConsumer(LOGGER));
  }

  protected Resource readFile(String fileName) {
    return new ClassPathResource("./requests/" + fileName);
  }
}
Şö

13 Eylül 2022 Salı

SpringTest Testcontainers MongoDBContainer

Giriş
MongoDBContainer yaratmak için tercih edilmeyen bazı yollar şöyle
// direct implementation
MongoDBContainer mongoDBContainer =
new MongoDBContainer(DockerImageName.parse("mongo:4.0.10")) //Bunu kullan
// generic implementation
MongoDBContainer mongoDBContainer =
new FixedHostPortGenericContainer("mongo:4.0.10"); //Bunu kullanma
Açıklaması şöyle. Yani esas sebep sabit port numarası kullanılması. Bu da testlerin paralel koşmasına mani.
Both versions should work. The difference is MongoDBContainer will run on a random port while with FixedHostPortGenericContainer you can use a port of your choosing. It simplifies connecting to Mongo a lot, cause you can enforce a good old localhost:27017 this way, so no dynamic configurations would be required.

The problem with this solution is it does not work well if you have multiple tests suits using non-shared container running at once. Processes will often kill MongoDB container in their teardown phase when other suits are already running or will try to start their Mongo and find out the port is already taken.
Örnek
Şöyle yaparız
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Testcontainers @Import(Configuration.class) public abstract class BaseApiTest { @Container static final MongoDBContainer mongo = new MongoDBContainer("mongo:3.6-xenial"); @DynamicPropertySource static void properties(DynamicPropertyRegistry registry) { registry.add("spring.data.mongodb.uri", mongo::getReplicaSetUrl); } @TestConfiguration public static class Configuration { @Lazy @Bean("testSetupMongoClient") public MongoClient testSetupMongoClient() { return MongoClients.create(mongo.getReplicaSetUrl("test")); } } }
Örnek
Şöyle yaparız. Burada Spock kullanılıyor.
@Testcontainers
@SpringBootTest
class SampleSuite extends Specification {
   @Shared
   static MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.0.18-xenial")

   @DynamicPropertySource
   static void mongoProps(DynamicPropertyRegistry registry) {
      mongoDBContainer.start()
      registry.add("spring.data.mongodb.uri", () ->   mongoDBContainer.replicaSetUrl)
   }
   void cleanup() {
      // cleaning up a bit should be a good idea...
      mongoDBContainer.stop()
   }
}

29 Ekim 2021 Cuma

SpringTest Testcontainers MySQLContainer

Maven
Örnek
Şöyle yaparız
<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>mysql</artifactId>
  <scope>test</scope>
</dependency>
Gradle
Örnek 
Şöyle yaparız
testImplementation("org.testcontainers:mysql")
testImplementation("org.testcontainers:spock")
testImplementation("org.testcontainers:testcontainers")
application-test.yml Dosyası
Örnek
Şöyle yaparız
datasources:
  default:
    url: jdbc:tc:mysql:8:///db
    driverClassName: org.testcontainers.jdbc.ContainerDatabaseDriver
Ayrıca testleri hızlandırmak için şöyle yaparız
testcontainers:
  reuse:
    enable: true
Açıklaması şöyle
Finally, in order to speed the testing process up, we should try to set a level of isolation in our tests that allow us to reuse the same image, which will reduce our test time dramatically:
MySQLContainer Sınıfı
Örnek
Şöyle yaparız
public static MySQLContainer mySQLContainer = new MySQLContainer("mysql:5.7")
  .withUsername("username")
  .withPassword("password")
  .withDatabaseName("test_db");
Örnek
Şöyle yaparız. Burada normalde container sınıfının start() metodunu çağırmaya gerek yok.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
@Testcontainers
class OrderControllerTest {

    static final MySQLContainer MY_SQL_CONTAINER;

    @Autowired
    WebTestClient webTestClient;

    @Autowired
    private OrderEntityRepository  orderEntityRepository;

    static {
        MY_SQL_CONTAINER = new MySQLContainer("mysql:latest");
        MY_SQL_CONTAINER.start();
    }

    @DynamicPropertySource
    static void configureTestProperties(DynamicPropertyRegistry registry){
      registry.add("spring.datasource.url",() -> MY_SQL_CONTAINER.getJdbcUrl());
      registry.add("spring.datasource.username",() -> MY_SQL_CONTAINER.getUsername());
      registry.add("spring.datasource.password",() -> MY_SQL_CONTAINER.getPassword());
      registry.add("spring.jpa.hibernate.ddl-auto",() -> "create");
  }
  ...
}    




28 Ağustos 2021 Cumartesi

SpringTest Testcontainers @TestContainers Anotasyonu

(disabledWithoutDocker Alanı
Örnek
Şöyle yaparız
@SpringBootTest
@TestContainers(disabledWithoutDocker = true)
@ContextConfiguration(initializers = ExampleIntegrationTests.Initializer.class)
class ExampleIntegrationTests {
  @Container
  public static CassandraContainer<?> cassandra = new CassandraContainer<>();
  
  static class Initializer 
    implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext context) {
      int cqlPort = cassandra.getMappedPort(CassandraContainer.CQL_PORT);
      TestPropertyValues.of("spring.data.cassandra.port="+ cqlPort)
        .applyTo(context.getEnvironment());
    }
  }
}

1 Ağustos 2021 Pazar

SpringTest Testcontainers PostgreSQLContainer

Giriş
Not : TestContainer yerine zonky kullanılabilir.

Test @TestContainers anotasyonu mecburen eklenir

Maven
Örnek
Şu satırı dahil ederiz
<properties>
  <testcontainers.version>1.16.0</testcontainers.version>
</properties>

<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>junit-jupiter</artifactId>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>postgresql</artifactId>
  <scope>test</scope>
</dependency>

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.testcontainers</groupId>
      <artifactId>testcontainers-bom</artifactId>
      <version>${testcontainers.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>
Örnek
Eğer dependencyManagement kullanmak istemiyorsak şöyle yaparız
<dependency>
   <groupId>org.testcontainers</groupId>
   <artifactId>testcontainers</artifactId>
   <version>1.16.3</version>
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>org.testcontainers</groupId>
   <artifactId>junit-jupiter</artifactId>
   <version>1.16.3</version>
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>org.testcontainers</groupId>
   <artifactId>postgresql</artifactId>
   <version>1.16.3</version>
   <scope>test</scope>
</dependency>
TestContainer ile PostgreSQL kullanmak için iki yöntem var.

1. Test veri tabanı bilgisi src/test/resources/application.yml altında saklanır
2. Teste @Container anotasyonu eklenir
3. TestContainer Sınıfından Kalıtma

1. Yml Kullanımı
Örnek
Şöyle yaparız
limizde şöyle bir application.yml olsun. Burada jdbc connection string "tc" ile başlıyor.
spring.datasource.url=jdbc:tc:postgresql://localhost/testdb
spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.datasource.username=user
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=update
Örnek
Elimizde şöyle bir application.yml olsun. Burada jdbc connection string "tc" ile başlıyor.
spring:
  datasource:
    url: jdbc:tc:postgresql:9.6.8:///test_database
    username: user
    password: password
  jpa:
    hibernate:
      ddl-auto: create
Açıklaması şöyle
Have you noticed the tc suffix in the JDBC-connection string? That’s the magic that comes with the union of JUnit 5 and Test containers. The thing is that you don’t need any programmatic configurations at all! When the framework sees that url contains the tc suffix it runs all necessary Docker commands internally.
Elimizde şöyle bir service kodu olsun
@Service
@RequiredArgsConstructor
public class PersonCreateServiceImpl implements PersonCreateService {

  private final PersonValidateService personValidateService;
  private final PersonRepository personRepository;

  @Override
  @Transactional
  public List<PersonDTO> createFamily(Iterable<String> firstNames, String lastName) {
    final var people = new ArrayList<PersonDTO>();
    firstNames.forEach(firstName -> people.add(createPerson(firstName, lastName)));
    return people;
  }
  
  @Override
  @Transactional
  public PersonDTO createPerson(String firstName, String lastName) {
    personValidateService.checkUserCreation(firstName, lastName);
    final var createdPerson = personRepository.saveAndFlush(
        new Person()
            .setFirstName(firstName)
            .setLastName(lastName)
    );
    return DTOConverters.toPersonDTO(createdPerson);
  }
}
Bu service koduna H2 veri tabanı ile test yazsaydık şöyle olurdu. Burada repository sınıfı ve service sınıfı @Autowired ile aynen kullanılıyor. Service sınıfının kullandığı PersonValidateService ile mock'lanıyor. @AutoConfigureTestDatabase ile test için veri tabanı yaratılıyor.
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@AutoConfigureTestDatabase
class PersonCreateServiceImplSpringBootTest {
  @Autowired
  private PersonRepository personRepository;
  @MockBean
  private PersonValidateService personValidateService;
  @Autowired
  private PersonCreateService personCreateService;

  @BeforeEach
  void init() {
    personRepository.deleteAll();
  }

  @Test
  void shouldCreateOnePerson() {
    final var people = personCreateService.createFamily(List.of("Simon"),"Kirekov");
    assertEquals(1, people.size());
    final var person = people.get(0);
    assertEquals("Simon", person.getFirstName());
    assertEquals("Kirekov", person.getLastName());
    assertTrue(person.getDateCreated().isBefore(ZonedDateTime.now()));
  }

  @Test
  void shouldRollbackIfAnyUserIsNotValidated() {
    doThrow(new ValidationFailedException(""))
        .when(personValidateService)
        .checkUserCreation("John", "Brown");
    assertThrows(ValidationFailedException.class, () -> personCreateService.createFamily(
        List.of("Matilda", "Vasya", "John"),
        "Brown"
    ));
    assertEquals(0, personRepository.count());
  }
}
Bu testi TestContainers ile çalışacak hale getirmek için şöyle yaparız. ActiveProfiles ile yaml dosyası kullanılır.
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@Testcontainers
@ActiveProfiles("test-containers")
class PersonCreateServiceImplTestContainers {
  @Autowired
  private PersonRepository personRepository;
  @MockBean
  private PersonValidateService personValidateService;
  @Autowired
  private PersonCreateService personCreateService;

  @BeforeEach
  void init() {
    personRepository.deleteAll();
  }
  @Test
  void shouldCreateOnePerson() {
    final var people = personCreateService.createFamily(
        List.of("Simon"),"Kirekov");
    assertEquals(1, people.size());
    final var person = people.get(0);
    assertEquals("Simon", person.getFirstName());
    assertEquals("Kirekov", person.getLastName());
    assertTrue(person.getDateCreated().isBefore(ZonedDateTime.now()));
  }
  @Test
  void shouldRollbackIfAnyUserIsNotValidated() {
    doThrow(new ValidationFailedException(""))
        .when(personValidateService)
        .checkUserCreation("John", "Brown");
    assertThrows(ValidationFailedException.class, () -> personCreateService.createFamily(
        List.of("Matilda", "Vasya", "John"),
        "Brown"
    ));
    assertEquals(0, personRepository.count());
  }
}
2. @Container Kullanımı
Repository sınıfı @Autowire ile test'e dahil edilir.

Örnek 
Şöyle yaparız. Burada PostgreSQLContainer() sürümü veriliyor
// JUnit 5 example with Spring Boot >= 2.2.6
@Testcontainers
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class ApplicationIT {

  @Container
  public static PostgreSQLContainer container = new PostgreSQLContainer("postgres:11.1")
    .withPassword("inmemory")
    .withUsername("inmemory");

  @DynamicPropertySource
  static void postgresqlProperties(DynamicPropertyRegistry registry) {
    registry.add("spring.datasource.url", container::getJdbcUrl);
    registry.add("spring.datasource.password", container::getPassword);
    registry.add("spring.datasource.username", container::getUsername);
  }
  ...
}
Eğer DataSource ta yaratmak istersek şöyle yaparız
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Testcontainers
@Import(Configuration.class)
public abstract class BaseApiTest {

  @Container
  static final PostgreSQLContainer<?> postgres = 
    new PostgreSQLContainer<>("postgres:10.9");
 
  @DynamicPropertySource
  static void properties(DynamicPropertyRegistry registry) {
    registry.add("spring.datasource.url", postgres::getJdbcUrl);
    registry.add("spring.datasource.username", postgres::getUsername);
    registry.add("spring.datasource.password", postgres::getPassword);
 
  }

  @TestConfiguration
  public static class Configuration {

    @Lazy
    @Bean("testSetupDataSource")
    public DataSource testInitDataSource() {
      PGSimpleDataSource ds = new PGSimpleDataSource();
      ds.setUrl(postgres.getJdbcUrl());
      ds.setDatabaseName(postgres.getDatabaseName());
      ds.setUser(postgres.getUsername());
      ds.setPassword(postgres.getPassword());
      return ds;
    }
  }
}
Örnek
Şöyle yaparız
@DataJdbcTest
@Import(DatasourceConfig.class) @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @Testcontainers @DisplayName("Comment Repository Integration Tests With TestContainers") class CommentRepositoryTest { @Container static PostgreSQLContainer<?> postgresContainer = new PostgreSQLContainer<>(DockerImageName.parse("postgres:15.1")); @DynamicPropertySource static void overridePostgresProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", postgresContainer::getJdbcUrl); registry.add("spring.datasource.username", postgresContainer::getUsername); registry.add("spring.datasource.password", postgresContainer::getPassword); registry.add("spring.flyway.url", postgresContainer::getJdbcUrl); } @Autowired private CommentRepository commentRepository; ... }
Açıklaması şöyle
@DataJdbcTest focuses only on Data JDBC components.
@Import(DatasourceConfig.class) provides JDBC auditing support.
@AutoConfigureTestDatabase disables in-memory test databases with AutoConfigureTestDatabase.Replace.NONE value. Because we want to use real database instance instead of in-memory database.
Örnek
Şöyle yaparız. Liquibase veri tabanını test için doldurur
@Testcontainers
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes =
ContainerSpringAfter226Test.class)
@EnableAutoConfiguration
public class ContainerSpringAfter226Test {

  @Container
  public static PostgreSQLContainer postgreSQLContainer =
SharedPostgresSQLContainer.getInstance()
    .withDatabaseName("dbname")
    .withUsername("user")
    .withPassword("password");

  @DynamicPropertySource
  public static void postgreSQLProperties(DynamicPropertyRegistry registry) {
    registry.add("spring.datasource.url", postgreSQLContainer::getJdbcUrl);
    registry.add("spring.datasource.username", postgreSQLContainer::getUsername);
    registry.add("spring.datasource.password", postgreSQLContainer::getPassword);

    registry.add("spring.liquibase.url", postgreSQLContainer::getJdbcUrl);
    registry.add("spring.liquibase.user", postgreSQLContainer::getUsername);
    registry.add("spring.liquibase.password", postgreSQLContainer::getPassword);
    registry.add("spring.liquibase.change-log",
() -> "classpath:/db/changelog/db.changelog-master.xml");
  }
  ...
}
3. TestContainer Sınıfından Kalıtma
Örnek
Şöyle yaparız
@TestConfiguration
public class PostgresSQLTestContainerConfig
  extends PostgreSQLContainer<PostgresSQLTestContainerConfig> {
  private static final String DOCKER_IMAGE_VERSION = "postgres:latest";

  private static final PostgresSQLTestContainerConfig POSTGRES_SQL_TEST_CONTAINER =
    new PostgresSQLTestContainerConfig().withReuse(true);

  static {
    POSTGRES_SQL_TEST_CONTAINER.start();
    System.setProperty("spring.datasource.url", POSTGRES_SQL_TEST_CONTAINER.getJdbcUrl());
    System.setProperty("spring.datasource.username",
      POSTGRES_SQL_TEST_CONTAINER.getUsername());
    System.setProperty("spring.datasource.password",
      POSTGRES_SQL_TEST_CONTAINER.getPassword());
  }

  public PostgresSQLTestContainerConfig() {
    super(DOCKER_IMAGE_VERSION);
  }

  @Override
  public void stop() {
    // do nothing, JVM handles shut down
  }
}


19 Ocak 2021 Salı

SpringTest Testcontainers Kullanımı

Giriş
Testcontainers  Spring initializr sayfasında var. Oradan eklemesi daha kolay. 
Maven
Örnek
Eğer elle eklemek istersek ana pom'u eklemek için yani BOM (Bill Of Materials) için şöyle yaparız
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.testcontainers</groupId>
      <artifactId>testcontainers-bom</artifactId>
      <version>${testcontainers.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>
Şu satırı dahil ederiz. junit-jupiter bağımlılığı eğer JUnit5 kullanıyorsak gerekiyor.
<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>testcontainers</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>junit-jupiter</artifactId>
  <scope>test</scope>
</dependency>
Daha sonra gerekli DB container'ı eklemek için şöyle yaparız. Burada JUnit5 desteği ve Neo4J ekleniyor
<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>neo4j</artifactId>
  <scope>test</scope>
</dependency>
Örnek
Eğer testcontainers için parent pom'u eklemek istemezsek şöyle yaparız
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>mongodb</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>spock</artifactId>
    <scope>test</scope>
</dependency>
Kullanım
1. Test sınıfına @Testcontainers anotasyonu eklenir
2. Test sınıfında @Container anotasyonu ile container tanımlanır
3. Container tarafından kullanılan property'ler @DynamicPropertySource ile spring'e tanıtılır.
4. Container tarafından kullanılan property'ler @ContextConfiguration ile spring'e tanıtılır.
5. Container tarafından kullanılacak property'ler test dizini altındaki application.properties'ten atanır

Örnek - @DynamicPropertySource
Şöyle yaparız
@SpringBootTest
@Testcontainers
public class PostRepositoryWithTestContainersTest {
  @Container
  static Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.0")
    .withStartupTimeout(Duration.ofMinutes(5));

  @DynamicPropertySource
  static void neo4jProperties(DynamicPropertyRegistry registry) {
    registry.add("spring.neo4j.uri", neo4jContainer::getBoltUrl);
    registry.add("spring.neo4j.authentication.username", () -> "neo4j");
    registry.add("spring.neo4j.authentication.password", neo4jContainer::getAdminPassword);
  }
  ...
}
Örnek - MySQLContainer 

Örnek - PostgreSQLContainer 

Örnek - MongoDBContainer

Örnek - RabitMQ
Bir örnek burada

Örnek - Couchbase
Şu satırı dahil ederiz
testImplementation("org.testcontainers:couchbase:1.16.3")
testImplementation("org.testcontainers:testcontainers:1.16.3")
testImplementation("org.testcontainers:junit-jupiter:1.16.3")

Örnek - @ContextConfiguration
Şöyle yaparız. Burada Spring için bir sürü ayar atanabiliyor.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = UserRepositoryTest.Initializer.class)
class UserRepositoryTest {

  @Autowired
  private UserRepository userRepository;
    
  @Test
  void findByUsername() {

    User john = ...;
    User savedUser = userRepository.save(john);

    assertEquals(1, savedUser.getId());
    assertEquals(john, userRepository.getOne(1L));
    assertEquals(john, userRepository.findByUsername("johnsmith"));

    User anna = ...;
    savedUser = userRepository.save(anna);

    assertEquals(2, savedUser.getId());
    assertEquals(anna, userRepository.getOne(2L));
    assertEquals(anna, userRepository.findByUsername("annasmith"));
  }
}
Ayarları atamak için şöyle yaparız. Burada Spring için bir sürü ayar atanabiliyor.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = UserRepositoryTest.Initializer.class)
class UserRepositoryTest {
  ...
  public static class Initializer implements
ApplicationContextInitializer<ConfigurableApplicationContext> {

    @Container
    private static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8");

    static {
      mysql.start();
    }

    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {

      TestPropertyValues.of(
        "spring.jpa.hibernate.ddl-auto=none",
        "spring.datasource.initialization-mode=always",
        "spring.datasource.platform=mysql",
        "spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true",
        "spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver",
        "spring.datasource.url=" + mysql.getJdbcUrl(),
        "spring.datasource.username=" + mysql.getUsername(),
        "spring.datasource.password=" + mysql.getPassword(),
        "spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect",
        "spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true",
        "spring.liquibase.change-log=classpath:/db/changelog/db.changelog-master.xml",
        "spring.liquibase.contexts=test",
        "logging.level.liquibase=INFO"
      ).applyTo(applicationContext);
    }
  }
}
TestContainer Nesnesinin Tüm Testlerde Paylaşılması
Örnek
Şöyle bir ata test sınıfı tanımlanır
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Testcontainers @Import(Configuration.class) public abstract class BaseApiTest { @Container static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:10.9"); @Container static final MongoDBContainer mongo = new MongoDBContainer("mongo:3.6-xenial"); @DynamicPropertySource static void properties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", postgres::getJdbcUrl); registry.add("spring.datasource.username", postgres::getUsername); registry.add("spring.datasource.password", postgres::getPassword); registry.add("spring.data.mongodb.uri", mongo::getReplicaSetUrl); } @TestConfiguration public static class Configuration { @Lazy @Bean("testSetupMongoClient") public MongoClient testSetupMongoClient() { return MongoClients.create(mongo.getReplicaSetUrl("test")); } @Lazy @Bean("testSetupDataSource") public DataSource testInitDataSource() { PGSimpleDataSource ds = new PGSimpleDataSource(); ds.setUrl(postgres.getJdbcUrl()); ds.setDatabaseName(postgres.getDatabaseName()); ds.setUser(postgres.getUsername()); ds.setPassword(postgres.getPassword()); return ds; } } }
İşleri kolaylaştırıcı şöyle anotasyonlar tanımlanır
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, 
  ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Autowired
@Qualifier("testSetupDataSource")
public @interface TestSetupDataSource {

}

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, 
  ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Autowired
@Qualifier("testSetupMongoClient")
public @interface TestSetupMongoClient {

}

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, 
  ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Autowired
@Qualifier("testSetupMongoClient")
public @interface TestSetupMongoClient {

}
Gerçek testler için ortak sınıf tanımlanır
public class ApiTest extends BaseApiTest {

  @Nested
  class Companies extends CompaniesApiTest {}

  @Nested
  class Esg extends EsgApiTest {}

  @Nested
  class Controversies extends ControversiesApiTest {}

}
Testler şöyledir
@TestInstance(Lifecycle.PER_CLASS)
abstract public class EsgApiTest {

  @TestSetupMongoClient
  private MongoClient mongoClient;

  @TestSetupDataSource
  private DataSource dataSource;

  @ApiBasePath
  private String apiBasePath;

  @BeforeAll
  public void beforeAll() throws SQLException {
    try (var con = dataSource.getConnection()) {
      var st = con.createStatement();
      st.executeUpdate("drop table if exists companies");
      st.executeUpdate("create table companies(id text, name text)");
      st.executeUpdate("insert into companies(id, name) values ('AAPL', 'Apple')");
      st.executeUpdate("insert into companies(id, name) values ('GOOGL', 'Google')");
    }
    Document apple = new Document(Map.of("_id", "AAPL", "...", 86,));
    Document google = new Document(Map.of("_id", "GOOGL", "...", 76));
    var scores = mongoClient.getDatabase("test").getCollection("scores");
    scores.drop();
    scores.insertMany(List.of(apple, google));
  }

  @Test
  void companyEsgScore() {
    //Burada rest çağrısı yapılabilir. Çünkü DB doludur
  }
}