Search code examples
javaspring-bootdockertestcontainers

Connect a Spring-Boot Application within a Testcontainer to a Postgres DB


currently I am testing MyService that has a dependency to the AccountService. For that I use @Testcontainers

setup

I face the problem that the AccountService can't connect to the database on my mac and I cant figure out why since the db is available and I can connect to it manually. I tried to connect MyService (not in a container, from IntelliJ) to the DB and it works fine.

I assume a configuration issue on the docker container for the AccountService due to this error: (see stacktrace below)

Connection refused: localhost/127.0.0.1:52801

where you hopefully can help me to solve this.

My test code

    @Testcontainers
    @SpringBootTest
    @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
    @AutoConfigureWebTestClient
    @EnableAutoConfiguration
    class MyServiceIntegrationTest {
    
        private static final String ACCOUNT_SERVICE = "account-service:0.0.1";
        private static final int ACCOUNT_SERVICE_INTERNAL_PORT = 8099;
        private static final String ACCOUNT_SERVICE_DATABASE_NAME = "account-db";
        private static final String DATABASE_USERNAME = "admin";
        private static final String DATABASE_PASSWORD = "password";
    
        @Container
        public static PostgreSQLContainer<?> accountServiceDb = new PostgreSQLContainer<>("postgres:latest")
                .withDatabaseName(ACCOUNT_SERVICE_DATABASE_NAME)
                .withUsername(DATABASE_USERNAME)
                .withPassword(DATABASE_PASSWORD);
        static GenericContainer<?> accountServiceContainer;
  
        @BeforeAll
        static void setup() {
        try (final Network network = Network.newNetwork()) {
            final var accountServiceEnv = Map.of(
                    "R2DBC_URL", "r2dbc:postgresql://"
                            + accountServiceDb.getHost() + ":"
                            + accountServiceDb.getFirstMappedPort()
                            + "/"
                            + accountServiceDb.getDatabaseName(),
                    "DB_USERNAME", DATABASE_USERNAME,
                    "DB_PASSWORD", DATABASE_PASSWORD
            );
    
            accountServiceContainer = new GenericContainer<>(DockerImageName.parse(ACCOUNT_SERVICE))
                    .withNetwork(network)
                    .withNetworkAliases(ACCOUNT_SERVICE)
                    .withEnv(accountServiceEnv)
                    .withExposedPorts(ACCOUNT_SERVICE_INTERNAL_PORT);
               
            accountServiceDb.withNetwork(network);
            accountServiceContainer.start();
        }
    }
        
        @Test
        void intTest(){
            // todo
        }
    }

Stacktrace from the AccountService

2023-02-22 09:16:27 2023-02-22 09:16:27.503  INFO 1 --- [           main] c.c.b.Account.AccountServiceApplication  : Starting AccountServiceApplication using Java 17.0.5 on 358da35fc9e0 with PID 1 (/service.jar started by root in /)
2023-02-22 09:16:27 2023-02-22 09:16:27.517  INFO 1 --- [           main] c.c.b.Account.AccountServiceApplication  : No active profile set, falling back to 1 default profile: "default"
2023-02-22 09:16:30 2023-02-22 09:16:30.833  INFO 1 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data R2DBC repositories in DEFAULT mode.
2023-02-22 09:16:30 2023-02-22 09:16:30.885  INFO 1 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 33 ms. Found 0 R2DBC repository interfaces.
2023-02-22 09:16:31 2023-02-22 09:16:31.567  INFO 1 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data R2DBC repositories in DEFAULT mode.
2023-02-22 09:16:32 2023-02-22 09:16:32.436  INFO 1 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 868 ms. Found 2 R2DBC repository interfaces.
2023-02-22 09:16:38 2023-02-22 09:16:38.398 ERROR 1 --- [tor-tcp-epoll-1] c.c.b.s.services.AccountBackendService   : Error by DB creation
2023-02-22 09:16:38 
2023-02-22 09:16:38 io.r2dbc.postgresql.PostgresqlConnectionFactory$PostgresConnectionException: Cannot connect to localhost/<unresolved>:52801
2023-02-22 09:16:38     at io.r2dbc.postgresql.PostgresqlConnectionFactory.cannotConnect(PostgresqlConnectionFactory.java:218) ~[r2dbc-postgresql-0.8.13.RELEASE.jar!/:0.8.13.RELEASE]
2023-02-22 09:16:38     at reactor.core.publisher.Mono.lambda$onErrorMap$31(Mono.java:3811) ~[reactor-core-3.4.26.jar!/:3.4.26]
2023-02-22 09:16:38     at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onError(FluxOnErrorResume.java:94) ~[reactor-core-3.4.26.jar!/:3.4.26]
2023-02-22 09:16:38     at 
SHORTENED
io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.87.Final.jar!/:4.1.87.Final]
2023-02-22 09:16:38     at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]
2023-02-22 09:16:38 Caused by: io.netty.channel.AbstractChannel$AnnotatedConnectException: finishConnect(..) failed: Connection refused: localhost/127.0.0.1:52801
2023-02-22 09:16:38 Caused by: java.net.ConnectException: finishConnect(..) failed: Connection refused
2023-02-22 09:16:38     at io.netty.channel.unix.Errors.newConnectException0(Errors.java:155) ~[netty-transport-native-unix-common-4.1.87.Final.jar!/:4.1.87.Final]
2023-02-22 09:16:38     at io.netty.channel.unix.Errors.handleConnectErrno(Errors.java:128) ~[netty-transport-native-unix-common-4.1.87.Final.jar!/:4.1.87.Final]
2023-02-22 09:16:38     at io.netty.channel.unix.Socket.finishConnect(Socket.java:359) ~[netty-transport-native-unix-common-4.1.87.Final.jar!/:4.1.87.Final]
2023-02-22 09:16:38     at io.netty.channel.epoll.AbstractEpollChannel$AbstractEpollUnsafe.doFinishConnect(AbstractEpollChannel.java:710) ~[netty-transport-classes-epoll-4.1.87.Final.jar!/:4.1.87.Final]
2023-02-22 09:16:38     at io.netty.channel.epoll.AbstractEpollChannel$AbstractEpollUnsafe.finishConnect(AbstractEpollChannel.java:687) ~[netty-transport-classes-epoll-4.1.87.Final.jar!/:4.1.87.Final]
2023-02-22 09:16:38     at io.netty.channel.epoll.AbstractEpollChannel$AbstractEpollUnsafe.epollOutReady(AbstractEpollChannel.java:567) ~[netty-transport-classes-epoll-4.1.87.Final.jar!/:4.1.87.Final]
2023-02-22 09:16:38     at io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:489) ~[netty-transport-classes-epoll-4.1.87.Final.jar!/:4.1.87.Final]
2023-02-22 09:16:38     at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:397) ~[netty-transport-classes-epoll-4.1.87.Final.jar!/:4.1.87.Final]
2023-02-22 09:16:38     at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997) ~[netty-common-4.1.87.Final.jar!/:4.1.87.Final]
2023-02-22 09:16:38     at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.87.Final.jar!/:4.1.87.Final]
2023-02-22 09:16:38     at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.87.Final.jar!/:4.1.87.Final]
2023-02-22 09:16:38     at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]
2023-02-22 09:16:38 
2023-02-22 09:16:38 2023-02-22 09:16:38.416 ERROR 1 --- [tor-tcp-epoll-1] reactor.core.publisher.Operators         : Operator called default onErrorDropped

Dockerfile of the AccountService

FROM secret/copenjdk17:17

EXPOSE 8080
EXPOSE 9090
# JMX port:
EXPOSE 9998
# Java Debug port:
EXPOSE 5004

COPY /build/libs/account-service-0.0.1.jar service.jar

STOPSIGNAL SIGINT

Thanks a lot in advance!


Solution

  • There are several things wrong in your code :

    • you need to connect both containers to the network
    • containers can be accessed over this network by the network alias which are DNS names over this network so you should use postgres container alias as host when connecting from service to this db
    • you should access postgres db over 5432 port and not the mapped port on the host because they communicate over the network.

    You would use mapped port if you want to connect from localhost Check below code with comments that should point you to right direction :

    @Testcontainers
    @SpringBootTest
    @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
    @AutoConfigureWebTestClient
    @EnableAutoConfiguration
    class MyServiceIntegrationTest {
    
        private static final String ACCOUNT_SERVICE = "account-service:0.0.1";
        private static final int ACCOUNT_SERVICE_INTERNAL_PORT = 8099;
        private static final String ACCOUNT_SERVICE_DATABASE_NAME = "account-db";
        private static final String DATABASE_USERNAME = "admin";
        private static final String DATABASE_PASSWORD = "password";
        //create network
        private static final Network network = Network.newNetwork();
    
        private static final String ACCOUNT_SERVICE_DB = "account-service-db";
        
        @Container
        public static PostgreSQLContainer<?> accountServiceDb = new PostgreSQLContainer<>("postgres:latest")
                .withDatabaseName(ACCOUNT_SERVICE_DATABASE_NAME)
                .withUsername(DATABASE_USERNAME)
                .withPassword(DATABASE_PASSWORD)
                //connect postgres db to network
                .withNetwork(network)
                //give the postgres DNS name
                .withNetworkAliases(ACCOUNT_SERVICE_DB);
        static GenericContainer<?> accountServiceContainer;
    
        @BeforeAll
        static void setup() {
            final Network network = Network.newNetwork();
            final var accountServiceEnv = Map.of(
                    "R2DBC_URL", "r2dbc:postgresql://"
                            //use db container DNS alias as host over common network
                            + ACCOUNT_SERVICE_DB + ":"
                            //here we use default postgres exposed port and not host port
                            + "5432"
                            + "/"
                            + accountServiceDb.getDatabaseName(),
                    "DB_USERNAME", DATABASE_USERNAME,
                    "DB_PASSWORD", DATABASE_PASSWORD
            );
    
            accountServiceContainer = new GenericContainer<>(DockerImageName.parse(ACCOUNT_SERVICE))
                    .withNetwork(network)
                    .withNetworkAliases(ACCOUNT_SERVICE)
                    .withEnv(accountServiceEnv)
                    .withExposedPorts(ACCOUNT_SERVICE_INTERNAL_PORT);
    
            accountServiceDb.withNetwork(network);
            accountServiceContainer.start();
        }
    
        @Test
        void intTest() {
            // todo
        }