Upload
boris-kravtsov
View
223
Download
0
Embed Size (px)
Citation preview
Integration testing with Docker Compose and JUnit
Dariusz Lorenc Boris Kravtsov
Pivotal Labs
Problem
End-2-end test of GemFire in various cluster configurations and with different failover scenarios
GemFire
GemFire is a distributed, in-memory database with strong data consistency, built to support transactional applications with low latency
and high concurrency needs
GemFire Server
GemFire
GemFire Server GemFire Server
Partitioned Data
Partitioned Data
Partitioned Data
GemFire Server GemFire Server GemFire Server
Partitioned Data
Partitioned Data
Partitioned Data
Client
WAN / Multi-Site
Gateway Hub
Gateway Hub
Relational Database
Docker Compose to the rescue
Docker Compose is a tool for defining and running multi-container Docker applications
Docker Compose JUnit Rule
A JUnit rule to manage docker containers using docker-compose
• Start and stop docker-compose multi-container applications
• Waits for services to become available before running tests
• Extends logging and debugging
https://github.com/palantir/docker-compose-rule
GreetingCounter
Master
Our Sample Distributed System
Test First - Happy Path
@Test public void shouldReturnData(){ get("/info") .then().assertThat() .body("counter", isA(Number.class)) .body("greeting", is("Hello World")); }
Spring Boot - Application.kt
@SpringBootApplication open class Application { companion object { @JvmStatic fun main(args: Array<String>) { SpringApplication.run(Application::class.java, *args) } } }
Greeting Service
@RestController class Controller { @RequestMapping("/greeting") fun greet(): Greeting { return Greeting("Hello World") } }
data class Greeting(val greeting: String)
Counter Service
@RestController class Controller { val counter = AtomicLong() @RequestMapping("/counter") fun count(): Counter { return Counter(counter.incrementAndGet()) } }
data class Counter(val counter: Long)
Master Service
@RestController class Controller @Autowired constructor( val greetingClient: GreetingClient, val counterClient: CounterClient) { @RequestMapping("/info") fun info(): Response { return Response(counterClient.counter().counter, greetingClient.greeting().greeting) } }
data class Response(val counter: Long, val greeting: String)
Master Service - Greeting Client
@FeignClient(name = "greeting", url = "http://greeting-service:8080") interface GreetingClient { @RequestMapping(value = "/greeting", method = arrayOf(RequestMethod.GET)) fun greeting(): Greeting }
Master Service - Counter Client
@FeignClient(name = "counter", url = "http://counter-service:8080") interface CounterClient { @RequestMapping(value = “/counter”, method = arrayOf(RequestMethod.GET)) fun counter(): Counter }
GreetingCounter
Master
Next Step: Dockerize Our Services
Dockerfiles
FROM frolvlad/alpine-oraclejdk8:slim
ADD master-service-1.0.0.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app.jar"]
Base image
Add the app’s fat jar
Expose the port
Start the java app
Build Docker Image with Gradle (Transmodo plugin)
buildscript { dependencies { classpath('se.transmode.gradle:gradle-docker:1.2') } } group = '<your docker group name>' apply plugin: 'docker' task buildDocker(type: Docker, dependsOn: build) { push = true applicationName = jar.baseName dockerfile = file('src/main/docker/Dockerfile') doFirst { copy { from jar into stageDir } } }
Gradle Docker plugin
Your Docker Hub ID
Build Docker task
GreetingCounter
MasterGreetingCount
“Docker Composed” Services
Docker Compose
docker-compose.ymlGreeting
Counter
Master
greeting-service: image: lorenc/greeting-service ports: - “8081:8080" counter-service: image: lorenc/counter-service ports: - "8082:8080" master-service: image: lorenc/master-service ports: - "8083:8080" links: - greeting-service - counter-service
Integration Test Setup
@Rule public DockerComposeRule docker = DockerComposeRule.builder() .file("../docker-compose.yml") .build();
@Before public void setUp() throws Exception { docker.dockerCompose().up(); } @After public void tearDown() throws Exception { docker.dockerCompose().down(); }
Health Checks & Logs
@Rule public DockerComposeRule docker = DockerComposeRule.builder() .file("../docker-compose.yml") .waitingForService("greeting-service", toRespondOverHttp(8080, TO_EXTERNAL_URI)) .waitingForService("counter-service", toRespondOverHttp(8080, TO_EXTERNAL_URI)) .waitingForService("master-service", toRespondOverHttp(8080, TO_EXTERNAL_URI)) .saveLogsTo("build/docker-logs") .build();
What could possibly go wrong?
@Test public void shouldReturnHolaWhenGreetingServiceDown(){ docker.dockerCompose().container("greeting-service").stop(); get("/info") .then().assertThat() .body("greeting", is("Hola!")); }
What could possibly go wrong?
@Test public void shouldReturn42WhenCounterServiceDown() { docker.dockerCompose().container("counter-service").stop(); get("/info") .then().assertThat() .body("counter", is(42)); }
@FeignClient(name = "greeting", url = "http://greeting-service:8080", fallback = DefaultGreeting::class) interface GreetingClient { @RequestMapping(value = "/greeting", method = arrayOf(RequestMethod.GET)) fun greeting(): Greeting }
@Component class DefaultGreeting : GreetingClient { override fun greeting(): Greeting { return Greeting("Hola!") } }
Master Service - Greeting Client
Master Service - Counter Client@FeignClient(name = "counter", url = "http://counter-service:8080", fallback = DefaultCounter::class) interface CounterClient { @RequestMapping(value = “/counter”, method = arrayOf(RequestMethod.GET)) fun counter(): Counter }
@Component class DefaultCounter : CounterClient { override fun counter(): Counter { return Counter(42) } }
Demo
References https://github.com/d-lorenc/junit-docker-demo https://github.com/palantir/docker-compose-rule