Como montar uma imagem do banco de dados Oracle para contêineres de teste

O código deve ser testado no DBMS com o qual ele trabalhará. Testcontainers é uma biblioteca que permite que você use praticamente qualquer DBMS em testes de unidade com a mesma facilidade que bancos de dados incorporados, como HSQLDB ou H2. Só haveria uma imagem do docker



Este artigo é dedicado à montagem de uma imagem da janela de encaixe conveniente para uso com os contêineres de teste. Quando tentei fazê-lo, tive problemas e aqui compartilho minha solução.
Vou coletar a imagem para o Oracle 11, porque é pequena e tenho a versão 11 suficiente. Com outras versões, a abordagem é praticamente a mesma.


Para deixar claro como usar a imagem, também haverá código Java que demonstra o uso da imagem para testar aplicativos Spring Boot. O método de conexão com os contêineres de teste que forneci provavelmente não é o melhor. Mas, primeiro, ele demonstra como usar as configurações especificadas ao criar a imagem. Em segundo lugar, é simples. E em terceiro lugar, ele quase não está vinculado ao Spring, pode até ficar preso no código Java, no qual não há nada além de main public void estático.


Supõe-se que o leitor esteja familiarizado superficialmente com o Docker e o Testcontaners e também conheça bem o Java. Para construir, você precisa usar o Linux, se estiver construindo no Windows, precisará usar o msys2 ou algo assim.


O código de demonstração é carregado no github aqui https://github.com/poxu/testcontainers-spring-demo Os scripts corrigidos para montar a imagem podem ser visualizados no meu fork das instruções de Oraklov https://github.com/poxu/docker-images/tree/ master / OracleDatabase / SingleInstance


Criar uma imagem do Docker


O Oracle não fornece imagens para o docker, mas publicou instruções detalhadas sobre como montá-las no github.


Infelizmente, não é possível usar essas imagens em contêineres de teste porque o contêiner que é iniciado a partir dessa imagem começa de dois a 20 minutos.


Para uso em testes de unidade, isso é inaceitável; portanto, é necessário fazer suas próprias alterações nos scripts, mas primeiro, é melhor tentar montar o contêiner de acordo com as instruções fornecidas pela Oracle. Farei uma breve recontagem aqui, uma instrução mais completa pode ser encontrada neste link https://github.com/oracle/docker-images/tree/master/OracleDatabase/SingleInstance


Montagem da imagem de acordo com as instruções do Oracle


Primeiro, você precisa clonar o repositório com instruções sobre como montar a imagem.


git clone https://github.com/oracle/docker-images.git 

Em seguida, obtenha o pacote rpm para a versão expressa autorizada do Oracle 11.2.0.2 Não é muito difícil, você só precisa se registrar no site da Oracle, acessar a página de download do Oracle DBMS, selecionar a versão 11.2.0.2 XE e baixar o arquivo rpm compactado oracle-xe-11.2.0 -1.0.x86 ~ 64 ~ .rpm.zip.


Coloque o arquivo no repositório git baixado no diretório docker-images / OracleDatabase / SingleInstance / dockerfiles / 11.2.0.2 /


Em seguida, acesse o diretório docker-images / OracleDatabase / SingleInstance / dockerfiles e execute o comando


 ./buildDockerImage.sh -v 11.2.0.2 -x 

A janela de encaixe montará uma imagem chamada oracle / database: 11.2.0.2-xe com base na qual você precisa criar o contêiner com este comando


 docker run --rm --name vanilla_oracle_test_habr \ -p 1234:1521 \ -p 5678:5500 \ -e ORACLE_PWD=123 \ --shm-size="1g" \ oracle/database:11.2.0.2-xe 

O contêiner inicia alguns minutos, porque, após iniciar, cria um novo banco de dados, e esse processo não é rápido.


Após alguns minutos, um banner aparecerá no console


############################
O BASE DE DADOS ESTÁ PRONTO PARA USAR!
############################

Depois disso, você pode se conectar ao banco de dados usando o logon do sistema, a senha é 123, o endereço de conexão é localhost e o SID é XE.


Se tudo funcionou, você pode prosseguir com o processo de criação de uma imagem em contêineres de teste. Caso contrário, é melhor primeiro ler o manual da Oracle e descobrir o que está errado.


Como já descobrimos, o contêiner é iniciado por um longo tempo devido ao fato de que, após o início, um banco de dados é criado. Em alguns casos, isso provavelmente pode ser conveniente, mas agora isso está causando danos completos. É necessário deixar o recipiente pronto para uso imediatamente após o lançamento.


Refinamento manual de imagens


Uma maneira de obter a imagem do banco de dados finalizado é aguardar até que o contêiner seja iniciado e a criação do banco de dados seja concluída e salve o contêiner em uma nova imagem.


É importante não iniciar o contêiner com o argumento --rm, caso contrário, a janela de encaixe o vencerá imediatamente após a parada.


 docker commit --message "container for unit tests" <container id> my/oracle-11.2.0.2-for-unit-tests 

Isso criará uma nova imagem a partir do contêiner, que será iniciada não apenas por alguns minutos, mas por 20 a 30 segundos.


Modificação do processo de montagem da imagem para obter uma imagem finalizada imediatamente após a montagem


Obviamente, é bom ter instruções para montar a imagem na forma de código, para que você possa iniciar a montagem com um comando e não seja necessário aguardar o início do contêiner e criar uma imagem com base nas suas mãos.


A última linha do arquivo docker indica o comando que é executado após o início


 CMD exec $ORACLE_BASE/$RUN_FILE 

\ $ ORACLE ~ BASE ~ / \ $ RUN ~ FILE ~ aponta para o arquivo docker-images / OracleDatabase / SingleInstance / dockerfiles / 11.2.0.2 / runOracle.sh


Se você parar esse contêiner e depois executá-lo novamente, como resultado, pela primeira vez, o script criará o banco de dados e, pela segunda vez, simplesmente o iniciará. Pode-se supor que, se você executar o script no estágio de montagem da imagem, a imagem será montada a partir do banco de dados já criado.


Mas, no caminho para implementar esse plano ousado, surge uma complicação.


O script é executado para sempre, ou seja, até que um sinal chegue ao contêiner de que o trabalho precisa ser concluído. Isso é resolvido de maneira bastante simples. A última linha do arquivo runOracle.sh contém o comando wait. Sabemos que, na fase de montagem, você não precisa esperar nada, precisa terminar o trabalho e, portanto, colocaremos uma declaração condicional no script.


Ele verificará se o argumento - execução durante a construção não é passado para o arquivo e se esse argumento é passado, não espere por nenhum sinal, mas simplesmente interrompa o trabalho. Ou seja, faça assim:


 if [ "$1" != "--running-while-building" ] then wait $childPID fi 

Bem, no dockerfile, adicionamos outra chamada de script, apenas no estágio de montagem. Acontecerá assim.


 RUN $ORACLE_BASE/$RUN_FILE --running-while-building CMD exec $ORACLE_BASE/$RUN_FILE 

Alterações necessárias para uso em testes


Eliminar o uso do volume


Precisa encontrar a linha


VOLUME ["\ $ ORACLE ~ BASE ~ / oradata"]

E comente sobre isso. Não há necessidade de usar volumes, porque todas as alterações serão lançadas após cada execução de teste, mas problemas com o uso de volumes podem surgir facilmente ao copiar imagens.


Excluir arquivos desnecessários


Precisa adicionar linhas


 rm -rf $ORACLE_HOME/demo && \ rm -rf $ORACLE_HOME/jdbc && \ rm -rf $ORACLE_HOME/jlib && \ rm -rf $ORACLE_HOME/md && \ rm -rf $ORACLE_HOME/nls/demo && \ rm -rf $ORACLE_HOME/odbc && \ rm -rf $ORACLE_HOME/rdbms/jlib && \ rm -rf $ORACLE_HOME/rdbms/public && \ rm -rf $ORACLE_HOME/rdbms/demo && \ rm -rf $ORACLE_HOME/bin/rman && \ 

Pouco antes da linha


 chmod ug+x $ORACLE_BASE/*.sh 

Isso removerá da imagem todos os arquivos que não são necessários para fins de teste. Quanto menor a imagem, melhor.


Remover divisão de imagem


Para reduzir a imagem, você precisa compilá-la usando o argumento squash . Isso removerá a separação em camadas da imagem, o que reduzirá ainda mais seu volume. O argumento squash é experimental, então você deve habilitá-lo separadamente. Em diferentes sistemas operacionais, isso é feito de maneiras diferentes.


Para encaminhar argumentos para a janela de encaixe, docker-images / OracleDatabase / SingleInstance / dockerfiles / buildDockerImage.sh fornece o argumento -o. Ou seja, para encaminhar o argumento --squash para a janela de encaixe, você precisa chamar buildDockerImage.sh assim


 ./buildDockerImage.sh -o '--squash' 

Alterar nome da imagem


Até o momento, a imagem é significativamente diferente do que a Oracle se propõe a fazer, portanto, deve ser renomeada. Para fazer isso, você já precisa editar o arquivo buildDockerImage.sh.O script leva o nome da imagem da variável IMAGE ~ NAME ~, cujo valor é definido diretamente no arquivo


Bem aqui


 # Oracle Database Image Name IMAGE_NAME="oracle/database:$VERSION-$EDITION" 

Eu mudei para


 # Oracle Database Image Name IMAGE_NAME="my/oracle-for-habr:$VERSION-$EDITION" 

Definir senha do banco de dados


Essa senha é definida na variável de ambiente ORACLE ~ PWD ~ durante o primeiro início do contêiner. Porém, como temos a configuração do banco de dados durante a criação da imagem, a variável precisa ser definida neste estágio. Se a capacidade de definir uma senha para cada montagem por meio da linha de comando não for necessária, você pode simplesmente inseri-la no dockerfile:


 ENV ORACLE_PWD=123 

Se, para algo que você precisa determinar a senha novamente a cada compilação, para encaminhar o argumento para a janela de encaixe, você pode novamente usar -o


 ./buildDockerImage.sh -v 11.2.0.2 -x -o '--squash --build-arg ORACLE_PWD=123' 

Isso transferirá a variável de ambiente ORACLE ~ PWD ~ para o dockerfile, mas o dockerfile não a passará para scripts executados durante a construção. Para que ele faça isso, você precisa adicionar a instrução ARG ao dockerfile.


 ARG ORACLE_PWD=default 

A senha, como provavelmente já ficou clara, será 123 e, se você não passar o ORACLE ~ PWD ~ para buildDockerImage.sh, será o padrão.


Às vezes, a Oracle acredita que a senha é incorreta e não deseja funcionar, portanto, pode ser necessário substituir 123 por outra.


Teste para elevar a imagem resultante


Agora você pode tentar executar o contêiner com base na imagem


 docker run --rm --name dockertest_habr \ -p 1234:1521 \ -p 5678:5500 \ --shm-size="1g" \ my/oracle-for-habr:11.2.0.2-xe 

O argumento --shm-size = "1g" é importante aqui, sem o qual o contêiner é iniciado, mas o próprio Oracle 11.2.0.2 não pode funcionar. Isso, apenas no caso, não significa que o contêiner precisará de um gigabyte de RAM, ele consome cerca de 100 megabytes.


Se o contêiner tiver aumentado normalmente, você pode tentar se conectar ao banco de dados, localizado lá.


Endereço base - provavelmente localhost
Porto - 1234
Usuário - SISTEMA
Senha - 123


Se iniciar normalmente, você poderá prosseguir para a próxima etapa.


Script de inicialização do banco de dados


Para que o programa possa trabalhar com o banco de dados a partir da imagem, é necessário que exista um circuito lá após o lançamento. Você pode criar esse circuito no estágio de construção, mas eu prefiro fazê-lo quando o contêiner iniciar.


Após o início, o contêiner procurará no diretório u01 / app / oracle / scripts / startup e executará todos os scripts sql que encontrar lá, que você poderá usar colocando o arquivo que criará o circuito. Algo assim.


 CREATE USER TEST_USER IDENTIFIED BY passwordnoquotes; ALTER USER TEST_USER QUOTA unlimited ON SYSTEM; GRANT CREATE SESSION, CONNECT, RESOURCE, DBA TO TEST_USER; GRANT ALL PRIVILEGES TO TEST_USER; 

Tudo isso precisa ser adicionado ao arquivo init ~ db ~ .sql, e o arquivo é lançado no contêiner usando -v


 docker run --rm --name dockertest_habr \ -p 1234:1521 \ -p 5678:5500 \ -e ORACLE_PWD=123 \ -v ${PWD}/init_db.sql:/u01/app/oracle/scripts/startup/init_db.sql \ --shm-size="1g" \ my/oracle-for-habr:11.2.0.2-xe 

\ $ {PWD} Aqui é usado porque o caminho absoluto para o arquivo é necessário. Ao usar o Windows, você precisa especificá-lo de alguma maneira diferente. Se, após iniciar, o esquema TEST ~ USER ~ foi criado com êxito, você pode continuar parafusando o contêiner recém-criado nos testes.


Usando uma imagem no código Java


Ao testar usando o banco de dados interno, via de regra, o mesmo problema surge. Se a configuração não puder ser retirada do cache, o Spring a coletará novamente. Em particular, ele recria o banco de dados interno, o que obviamente atrasa seriamente os testes. Resolvi o problema com força bruta, simplesmente transformando a parte de elevação do contêiner em um singleton. Real, condomínio.


Para o Oracle XE, o Testcontainers possui uma classe especialmente preparada. Antes de tudo, essa classe sabe que estamos falando de um contêiner com um DBMS e que, para determinar se ele foi gerado, devemos tentar conectar-se ao banco de dados usando o jdbc.


Um objeto desta classe aguardará o levantamento do contêiner por si só, você só precisa dizer quais logs com senha usar.


 import org.testcontainers.containers.BindMode; import org.testcontainers.containers.OracleContainer; public class StaticOracleContainer { public static OracleContainer getContainer() { return LazyOracleContainer.ORACLE_CONTAINER; } private static class LazyOracleContainer { private static final OracleContainer ORACLE_CONTAINER = makeContainer(); private static OracleContainer makeContainer() { //       testcontainers.properties //  oracle.container.image final var dockerImageName = "my/oracle-for-habr"; final var container = new OracleContainer(dockerImageName) //    ,  testcontainers //  ,  ,  //   .withUsername("SYSTEM").withPassword("123") // ,  testcontainers  //     .withExposedPorts(1521, 5500) //      shared memory //    .withSharedMemorySize(2147483648L) //   ,       // -v /path/to/init_db.sql:/u01/app/oracle/scripts/startup/init_db.sql //    init_db.sql   .withClasspathResourceMapping("init_db.sql" , "/u01/app/oracle/scripts/startup/init_db.sql" , BindMode.READ_ONLY); container.start(); return container; } } } 

Além disso, os contêineres de teste, quando iniciados, mapeiam as portas internas do contêiner para portas externas desocupadas definidas aleatoriamente. Portanto, você não pode ter medo de que o contêiner não suba, porque a porta já está sendo usada por alguém. Uma porta externa pode ser obtida do contêiner usando o método getOraclePort ().


Você também pode obter o endereço do contêiner usando o método getContainerIpAddress (), mas qualquer contêiner possui esse método.


Após a primeira chamada ao método getContainer, o contêiner não será recriado, mas o existente será retornado. Agora, esse método pode ser usado em Java simples ou na configuração do Spring para obter um objeto com um contêiner do qual você pode extrair as portas e o endereço da conexão.


Por exemplo, você pode criar um inicializador que, ao elevar o contexto, substituirá os atributos de mola responsáveis ​​pela conexão com o banco de dados.


Classe para anexar contêineres de teste ao Spring


Após elevar o contêiner, o inicializador substitui as propriedades responsáveis ​​pela URL para conectar-se ao banco de dados, login, senha e tudo mais.


Mas, se não houver configuração evilcorp.testcontainers.enabled em application.properties, o contêiner não será levantado e tudo funcionará como se ninguém conectasse contêineres de teste.


 package com.evilcorp.demo; import org.junit.platform.commons.logging.Logger; import org.junit.platform.commons.logging.LoggerFactory; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.testcontainers.containers.OracleContainer; public class TestcontainersInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { static Logger log = LoggerFactory.getLogger(TestcontainersInitializer.class); @Override public void initialize(ConfigurableApplicationContext applicationContext) { // ,   Testcontainers final String testcontainersEnabled = applicationContext.getEnvironment().getProperty("evilcorp.testcontainers.enabled"); if (!"true".equals(testcontainersEnabled)) { return; } OracleContainer oracleContainer = StaticOracleContainer.getContainer(); oracleContainer.followOutput(s -> log.debug(() -> s.getUtf8String())); // IP       //  ,    // (linux, MacOs, Windows  ) , //    oracleContainer.getContainerIpAddress() //    // // Testcontainers     //   ,    oracleContainer.getOraclePort() //  ,         final String jdbcUrl = "jdbc:oracle:thin:@//" + oracleContainer.getContainerIpAddress() + ":" + oracleContainer.getOraclePort() + "/XE"; //      init_db.sql //      final String user = "TEST_USER"; final String password = "passwordnoquotes"; TestPropertyValues.of( "spring.jpa.properties.hibernate.default_schema=" + user, "spring.datasource.driver-class-name=oracle.jdbc.OracleDriver", "spring.jpa.database-platform=org.hibernate.dialect.Oracle10gDialect", "spring.datasource.username=" + user, "spring.datasource.password=" + password, "spring.datasource.url=" + jdbcUrl, "spring.liquibase.url=" + jdbcUrl, "spring.liquibase.user=" + user, "spring.liquibase.password=" + password ).applyTo(applicationContext.getEnvironment(), TestPropertyValues.Type.MAP, "test"); } } 

Essa configuração pode ser usada no teste de inicialização da primavera para substituir as configurações do banco de dados em tempo real.


Teste usando contêineres de teste


O teste simplesmente grava um objeto no banco de dados e o lê, nada de especial.


 package com.evilcorp.demo; import com.evilcorp.demo.entity.User; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ContextConfiguration; import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertTrue; @SpringBootTest @ContextConfiguration(initializers = {TestcontainersInitializer.class}) class TestcontainersSpringDemoApplicationTests { @Autowired UserRepository userRepository; private User createdUser; @BeforeEach void setUp() { createdUser = new User(); createdUser.setName("Fry"); userRepository.save(createdUser); } @Test void userRepositoryLoaded() { assertNotNull(userRepository); } @Test void userAdded() { final Optional<User> loadedUser = userRepository.findById(createdUser.getUserId()); assertTrue(loadedUser.isPresent()); assertEquals("Fry", loadedUser.get().getName()); assertNotSame(createdUser, loadedUser.get()); } } 

E sim, adicione dependências ao pom.xml


  <dependency> <groupId>org.testcontainers</groupId> <artifactId>testcontainers</artifactId> <version>1.12.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>oracle-xe</artifactId> <version>1.12.3</version> <scope>test</scope> </dependency> 

É sobre como você pode criar uma imagem de janela de encaixe Oracle DBMS e usá-la no código Java. Resta apenas colocar a imagem no repositório corporativo de artefatos e organizar o lançamento de testes dentro de outro contêiner de docker. Mas esta é uma história completamente diferente.

Source: https://habr.com/ru/post/pt480106/


All Articles