diff --git a/docker-compose.yml b/docker-compose.yml index 9c34d2a33..f949dc915 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,8 @@ services: - MYSQL_DATABASE=petclinic volumes: - "./conf.d:/etc/mysql/conf.d:ro" + profiles: + - mysql postgres: image: postgres:15.3 ports: @@ -21,3 +23,5 @@ services: - POSTGRES_PASSWORD=petclinic - POSTGRES_USER=petclinic - POSTGRES_DB=petclinic + profiles: + - postgres diff --git a/pom.xml b/pom.xml index 3f330a9ee..ab6215445 100644 --- a/pom.xml +++ b/pom.xml @@ -110,7 +110,27 @@ org.springframework.boot spring-boot-devtools - true + test + + + org.springframework.boot + spring-boot-testcontainers + test + + + org.springframework.boot + spring-boot-docker-compose + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + mysql + test @@ -120,6 +140,18 @@ + + + + org.testcontainers + testcontainers-bom + ${testcontainers.version} + pom + import + + + + diff --git a/readme.md b/readme.md index d3796fd24..80d80090c 100644 --- a/readme.md +++ b/readme.md @@ -66,6 +66,22 @@ docker run -e POSTGRES_USER=petclinic -e POSTGRES_PASSWORD=petclinic -e POSTGRES Further documentation is provided for [MySQL](https://github.com/spring-projects/spring-petclinic/blob/main/src/main/resources/db/mysql/petclinic_db_setup_mysql.txt) and for [PostgreSQL](https://github.com/spring-projects/spring-petclinic/blob/main/src/main/resources/db/postgres/petclinic_db_setup_postgres.txt). +Instead of vanilla `docker` you can also use the provided `docker-compose.yml` file to start the database containers. Each one has a profile just like the Spring profile: + +``` +$ docker-compose --profile mysql up +``` + +or + +``` +$ docker-compose --profile postgres up +``` + +## Test Applications + +At development time we recommend you use the test applications set up as `main()` methods in `PetClinicIntegrationTests` (using the default H2 database and also adding Spring Boot devtools), `MySqlTestApplication` and `PostgresIntegrationTests`. These are set up so that you can run the apps in your IDE and get fast feedback, and also run the same classes as integration tests against the respective database. The MySql integration tests use Testcontainers to start the database in a Docker container, and the Postgres tests use Docker Compose to do the same thing. + ## Compiling the CSS There is a `petclinic.css` in `src/main/resources/static/resources/css`. It was generated from the `petclinic.scss` source, combined with the [Bootstrap](https://getbootstrap.com/) library. If you make changes to the `scss`, or upgrade Bootstrap, you will need to re-compile the CSS resources using the Maven profile "css", i.e. `./mvnw package -P css`. There is no build profile for Gradle to compile the CSS. diff --git a/src/test/java/org/springframework/samples/petclinic/MySqlIntegrationTests.java b/src/test/java/org/springframework/samples/petclinic/MySqlIntegrationTests.java new file mode 100644 index 000000000..b78694cf5 --- /dev/null +++ b/src/test/java/org/springframework/samples/petclinic/MySqlIntegrationTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.samples.petclinic; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.HttpStatus; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.samples.petclinic.vet.VetRepository; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.web.client.RestTemplate; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@ActiveProfiles("mysql") +@Testcontainers +class MySqlIntegrationTests { + + @ServiceConnection + @Container + static MySQLContainer container = new MySQLContainer<>("mysql:5.7"); + + @LocalServerPort + int port; + + @Autowired + private VetRepository vets; + + @Autowired + private RestTemplateBuilder builder; + + @Test + void testFindAll() throws Exception { + vets.findAll(); + vets.findAll(); // served from cache + } + + @Test + void testOwnerDetails() { + RestTemplate template = builder.rootUri("http://localhost:" + port).build(); + ResponseEntity result = template.exchange(RequestEntity.get("/owners/1").build(), String.class); + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); + } + +} diff --git a/src/test/java/org/springframework/samples/petclinic/MysqlTestApplication.java b/src/test/java/org/springframework/samples/petclinic/MysqlTestApplication.java new file mode 100644 index 000000000..3df841848 --- /dev/null +++ b/src/test/java/org/springframework/samples/petclinic/MysqlTestApplication.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.samples.petclinic; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.testcontainers.containers.MySQLContainer; + +/** + * PetClinic Spring Boot Application. + * + * @author Dave Syer + * + */ +@Configuration +public class MysqlTestApplication { + + @ServiceConnection + @Profile("mysql") + @Bean + static MySQLContainer container() { + return new MySQLContainer<>("mysql:5.7"); + } + + public static void main(String[] args) { + SpringApplication.run(PetClinicApplication.class, "--spring.profiles.active=mysql"); + } + +} diff --git a/src/test/java/org/springframework/samples/petclinic/PetClinicIntegrationTests.java b/src/test/java/org/springframework/samples/petclinic/PetClinicIntegrationTests.java index 3bf1c0ca1..6472a1212 100644 --- a/src/test/java/org/springframework/samples/petclinic/PetClinicIntegrationTests.java +++ b/src/test/java/org/springframework/samples/petclinic/PetClinicIntegrationTests.java @@ -20,6 +20,7 @@ import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalServerPort; @@ -31,7 +32,7 @@ import org.springframework.samples.petclinic.vet.VetRepository; import org.springframework.web.client.RestTemplate; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -class PetClinicIntegrationTests { +public class PetClinicIntegrationTests { @LocalServerPort int port; @@ -55,4 +56,8 @@ class PetClinicIntegrationTests { assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); } + public static void main(String[] args) { + SpringApplication.run(PetClinicApplication.class, args); + } + } diff --git a/src/test/java/org/springframework/samples/petclinic/PostgresIntegrationTests.java b/src/test/java/org/springframework/samples/petclinic/PostgresIntegrationTests.java new file mode 100644 index 000000000..5053e7ada --- /dev/null +++ b/src/test/java/org/springframework/samples/petclinic/PostgresIntegrationTests.java @@ -0,0 +1,130 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.samples.petclinic; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.context.event.ApplicationPreparedEvent; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.ApplicationListener; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.http.HttpStatus; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.samples.petclinic.vet.VetRepository; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.web.client.RestTemplate; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "spring.docker.compose.skip.in-tests=false", // + "spring.docker.compose.profiles.active=postgres" }) +@ActiveProfiles("postgres") +public class PostgresIntegrationTests { + + @LocalServerPort + int port; + + @Autowired + private VetRepository vets; + + @Autowired + private RestTemplateBuilder builder; + + public static void main(String[] args) { + new SpringApplicationBuilder(PetClinicApplication.class) // + .profiles("postgres") // + .properties( // + "spring.docker.compose.profiles.active=postgres" // + ) // + .listeners(new PropertiesLogger()) // + .run(args); + } + + @Test + void testFindAll() throws Exception { + vets.findAll(); + vets.findAll(); // served from cache + } + + @Test + void testOwnerDetails() { + RestTemplate template = builder.rootUri("http://localhost:" + port).build(); + ResponseEntity result = template.exchange(RequestEntity.get("/owners/1").build(), String.class); + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + static class PropertiesLogger implements ApplicationListener { + + private static final Log log = LogFactory.getLog(PropertiesLogger.class); + + private ConfigurableEnvironment environment; + + private boolean isFirstRun = true; + + @Override + public void onApplicationEvent(ApplicationPreparedEvent event) { + if (isFirstRun) { + environment = event.getApplicationContext().getEnvironment(); + printProperties(); + } + isFirstRun = false; + } + + public void printProperties() { + for (EnumerablePropertySource source : findPropertiesPropertySources()) { + log.info("PropertySource: " + source.getName()); + String[] names = source.getPropertyNames(); + Arrays.sort(names); + for (String name : names) { + String resolved = environment.getProperty(name); + String value = source.getProperty(name).toString(); + if (resolved.equals(value)) { + log.info(name + "=" + resolved); + } + else { + log.info(name + "=" + value + " OVERRIDDEN to " + resolved); + } + } + } + } + + private List> findPropertiesPropertySources() { + List> sources = new LinkedList<>(); + for (PropertySource source : environment.getPropertySources()) { + if (source instanceof EnumerablePropertySource enumerable) { + sources.add(enumerable); + } + } + return sources; + } + + } + +}