├── disque-job-queue ├── src │ ├── main │ │ └── java │ │ │ ├── MailApiClient.java │ │ │ ├── MailMessage.java │ │ │ ├── MailSenderService.java │ │ │ └── DisqueBackedMailSenderService.java │ └── test │ │ ├── resources │ │ └── logback-test.xml │ │ └── java │ │ ├── SingleDisqueInstanceTest.java │ │ └── DisqueBackedMailSenderTest.java └── pom.xml ├── spring-boot ├── src │ ├── test │ │ ├── resources │ │ │ └── logback-test.xml │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ ├── DemoControllerTest.java │ │ │ └── AbstractIntegrationTest.java │ └── main │ │ └── java │ │ └── com │ │ └── example │ │ ├── DemoApplication.java │ │ └── DemoController.java └── pom.xml ├── circle.yml ├── linked-container ├── src │ ├── test │ │ ├── resources │ │ │ └── logback-test.xml │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ └── linkedcontainer │ │ │ ├── RedmineContainer.java │ │ │ └── RedmineClientTest.java │ └── main │ │ └── java │ │ └── com │ │ └── example │ │ └── linkedcontainer │ │ └── RedmineClient.java └── pom.xml ├── redis-backed-cache ├── src │ ├── test │ │ ├── resources │ │ │ └── logback-test.xml │ │ └── java │ │ │ └── RedisBackedCacheTest.java │ └── main │ │ └── java │ │ └── com │ │ └── mycompany │ │ └── cache │ │ ├── Cache.java │ │ └── RedisBackedCache.java └── pom.xml ├── selenium-container ├── src │ └── test │ │ ├── resources │ │ └── logback-test.xml │ │ └── java │ │ └── SeleniumContainerTest.java └── pom.xml ├── README.md ├── .gitignore └── pom.xml /disque-job-queue/src/main/java/MailApiClient.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by rnorth on 03/01/2016. 3 | */ 4 | public interface MailApiClient { 5 | 6 | boolean send(MailMessage message) throws MailSendException; 7 | 8 | class MailSendException extends RuntimeException { 9 | 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /spring-boot/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /disque-job-queue/src/main/java/MailMessage.java: -------------------------------------------------------------------------------- 1 | import lombok.AllArgsConstructor; 2 | import lombok.Data; 3 | 4 | /** 5 | * Created by rnorth on 03/01/2016. 6 | */ 7 | @Data @AllArgsConstructor 8 | public class MailMessage { 9 | public final String subject; 10 | public final String recipient; 11 | public final String body; 12 | } 13 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | java: 3 | version: oraclejdk8 4 | environment: 5 | DOCKER_HOST: tcp://127.0.0.1:2376 6 | DOCKER_TLS_VERIFY: 0 7 | MAVEN_OPTS: -Xmx512m 8 | dependencies: 9 | override: 10 | - "sudo docker -d -e lxc -s btrfs -H 0.0.0.0:2376 -H unix:///var/run/docker.sock --log-level=debug --debug=true": 11 | background: true 12 | - mvn install -DskipTests -------------------------------------------------------------------------------- /spring-boot/src/main/java/com/example/DemoApplication.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class DemoApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(DemoApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /linked-container/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /disque-job-queue/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /redis-backed-cache/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /selenium-container/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /spring-boot/src/main/java/com/example/DemoController.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import org.springframework.data.redis.core.StringRedisTemplate; 4 | import org.springframework.web.bind.annotation.*; 5 | 6 | @RestController 7 | public class DemoController { 8 | 9 | private final StringRedisTemplate stringRedisTemplate; 10 | 11 | public DemoController(StringRedisTemplate stringRedisTemplate){ 12 | this.stringRedisTemplate = stringRedisTemplate; 13 | } 14 | 15 | @GetMapping("/foo") 16 | public String get() { 17 | return stringRedisTemplate.opsForValue().get("foo"); 18 | } 19 | 20 | @PutMapping("/foo") 21 | public void set(@RequestBody String value) { 22 | stringRedisTemplate.opsForValue().set("foo", value); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /spring-boot/src/test/java/com/example/DemoControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import static org.rnorth.visibleassertions.VisibleAssertions.*; 4 | 5 | import org.junit.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.web.client.TestRestTemplate; 8 | 9 | public class DemoControllerTest extends AbstractIntegrationTest { 10 | 11 | @Autowired 12 | TestRestTemplate restTemplate; 13 | 14 | @Test 15 | public void simpleTest() { 16 | String fooResource = "/foo"; 17 | 18 | info("putting 'bar' to " + fooResource); 19 | restTemplate.put(fooResource, "bar"); 20 | 21 | assertEquals("value is set", "bar", restTemplate.getForObject(fooResource, String.class)); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /linked-container/src/main/java/com/example/linkedcontainer/RedmineClient.java: -------------------------------------------------------------------------------- 1 | package com.example.linkedcontainer; 2 | 3 | import okhttp3.OkHttpClient; 4 | import okhttp3.Request; 5 | import okhttp3.Response; 6 | import org.json.JSONObject; 7 | 8 | import java.io.IOException; 9 | 10 | /** 11 | * A crude, partially implemented Redmine client. 12 | */ 13 | public class RedmineClient { 14 | 15 | private String url; 16 | private OkHttpClient client; 17 | 18 | public RedmineClient(String url) { 19 | this.url = url; 20 | client = new OkHttpClient(); 21 | } 22 | 23 | public int getIssueCount() throws IOException { 24 | Request request = new Request.Builder() 25 | .url(url + "/issues.json") 26 | .build(); 27 | 28 | Response response = client.newCall(request).execute(); 29 | JSONObject jsonObject = new JSONObject(response.body().string()); 30 | return jsonObject.getInt("total_count"); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # testcontainers-java-examples 2 | 3 | # ARCHIVED! Please see [examples in the main testcontainers-java repository](https://github.com/testcontainers/testcontainers-java/tree/master/examples) 4 | 5 | > Practical examples of using Testcontainers. 6 | 7 | The code in this repository accompanies the following blog posts: 8 | 9 | * [JUnit integration testing with Docker and Testcontainers](https://rnorth.org/junit-integration-testing-with-docker-and-testcontainers) 10 | * [Fun with Disque, Java and Spinach](https://rnorth.org/fun-with-disque-java-and-spinach) 11 | * [Better JUnit Selenium testing with Docker and Testcontainers](https://rnorth.org/better-junit-selenium-testing-with-docker-and-testcontainers) 12 | 13 | Note that this examples project uses JitPack to obtain SNAPSHOT versions of the latest build of TestContainers. For 14 | normal usage, refer to the [documentation](http://testcontainers.viewdocs.io/testcontainers-java/#user-content-maven-dependencies) 15 | for setting the correct maven dependencies for your project. 16 | -------------------------------------------------------------------------------- /redis-backed-cache/src/main/java/com/mycompany/cache/Cache.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.cache; 2 | 3 | import java.util.Optional; 4 | 5 | /** 6 | * Cache, for storing data associated with keys. 7 | */ 8 | public interface Cache { 9 | 10 | /** 11 | * Store a value object in the cache with no specific expiry time. The object may be evicted by the cache any time, 12 | * if necessary. 13 | * 14 | * @param key key that may be used to retrieve the object in the future 15 | * @param value the value object to be stored 16 | */ 17 | void put(String key, Object value); 18 | 19 | /** 20 | * Retrieve a value object from the cache. 21 | * @param key the key that was used to insert the object initially 22 | * @param expectedClass for convenience, a class that the object should be cast to before being returned 23 | * @param the class of the returned object 24 | * @return the object if it was in the cache, or an empty Optional if not found. 25 | */ 26 | Optional get(String key, Class expectedClass); 27 | } -------------------------------------------------------------------------------- /linked-container/src/test/java/com/example/linkedcontainer/RedmineContainer.java: -------------------------------------------------------------------------------- 1 | package com.example.linkedcontainer; 2 | 3 | import org.testcontainers.containers.GenericContainer; 4 | import org.testcontainers.containers.traits.LinkableContainer; 5 | import org.testcontainers.containers.wait.Wait; 6 | 7 | /** 8 | * A Redmine container. 9 | */ 10 | public class RedmineContainer extends GenericContainer { 11 | 12 | private static final int REDMINE_PORT = 3000; 13 | 14 | public RedmineContainer(String dockerImageName) { 15 | super(dockerImageName); 16 | } 17 | 18 | @Override 19 | protected void configure() { 20 | addExposedPort(REDMINE_PORT); 21 | waitingFor(Wait.forHttp("/")); 22 | } 23 | 24 | public RedmineContainer withLinkToContainer(LinkableContainer otherContainer, String alias) { 25 | addLink(otherContainer, alias); 26 | return this; 27 | } 28 | 29 | public String getRedmineUrl() { 30 | return String.format("http://%s:%d", 31 | this.getContainerIpAddress(), 32 | this.getMappedPort(REDMINE_PORT)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /redis-backed-cache/src/main/java/com/mycompany/cache/RedisBackedCache.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.cache; 2 | 3 | import com.google.gson.Gson; 4 | import redis.clients.jedis.Jedis; 5 | 6 | import java.util.Optional; 7 | 8 | /** 9 | * An implementation of {@link Cache} that stores data in Redis. 10 | */ 11 | public class RedisBackedCache implements Cache { 12 | 13 | private final Jedis jedis; 14 | private final String cacheName; 15 | private final Gson gson; 16 | 17 | public RedisBackedCache(Jedis jedis, String cacheName) { 18 | this.jedis = jedis; 19 | this.cacheName = cacheName; 20 | this.gson = new Gson(); 21 | } 22 | 23 | @Override 24 | public void put(String key, Object value) { 25 | String jsonValue = gson.toJson(value); 26 | this.jedis.hset(this.cacheName, key, jsonValue); 27 | } 28 | 29 | @Override 30 | public Optional get(String key, Class expectedClass) { 31 | String foundJson = this.jedis.hget(this.cacheName, key); 32 | 33 | if (foundJson == null) { 34 | return Optional.empty(); 35 | } 36 | 37 | return Optional.of(gson.fromJson(foundJson, expectedClass)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Maven template 3 | target/ 4 | pom.xml.tag 5 | pom.xml.releaseBackup 6 | pom.xml.versionsBackup 7 | pom.xml.next 8 | release.properties 9 | dependency-reduced-pom.xml 10 | buildNumber.properties 11 | .mvn/timing.properties 12 | ### JetBrains template 13 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 14 | 15 | *.iml 16 | 17 | ## Directory-based project format: 18 | .idea/ 19 | # if you remove the above rule, at least ignore the following: 20 | 21 | # User-specific stuff: 22 | # .idea/workspace.xml 23 | # .idea/tasks.xml 24 | # .idea/dictionaries 25 | 26 | # Sensitive or high-churn files: 27 | # .idea/dataSources.ids 28 | # .idea/dataSources.xml 29 | # .idea/sqlDataSources.xml 30 | # .idea/dynamic.xml 31 | # .idea/uiDesigner.xml 32 | 33 | # Gradle: 34 | # .idea/gradle.xml 35 | # .idea/libraries 36 | 37 | # Mongo Explorer plugin: 38 | # .idea/mongoSettings.xml 39 | 40 | ## File-based project format: 41 | *.ipr 42 | *.iws 43 | 44 | ## Plugin-specific files: 45 | 46 | # IntelliJ 47 | /out/ 48 | 49 | # mpeltonen/sbt-idea plugin 50 | .idea_modules/ 51 | 52 | # JIRA plugin 53 | atlassian-ide-plugin.xml 54 | 55 | # Crashlytics plugin (for Android Studio and IntelliJ) 56 | com_crashlytics_export_strings.xml 57 | crashlytics.properties 58 | crashlytics-build.properties 59 | 60 | # Eclipse 61 | .project 62 | .classpath 63 | .settings 64 | bin 65 | 66 | -------------------------------------------------------------------------------- /selenium-container/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | testcontainers-java-examples 7 | org.testcontainers.examples 8 | NOVERSION 9 | 10 | 4.0.0 11 | 12 | selenium-container 13 | 14 | 15 | 16 | org.seleniumhq.selenium 17 | selenium-remote-driver 18 | 2.45.0 19 | 20 | 21 | ${testcontainers.group} 22 | selenium 23 | ${testcontainers.version} 24 | 25 | 26 | org.slf4j 27 | slf4j-api 28 | 1.7.7 29 | provided 30 | 31 | 32 | ch.qos.logback 33 | logback-classic 34 | 1.1.2 35 | test 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /disque-job-queue/src/main/java/MailSenderService.java: -------------------------------------------------------------------------------- 1 | import lombok.Data; 2 | import lombok.RequiredArgsConstructor; 3 | 4 | import java.util.concurrent.Callable; 5 | 6 | /** 7 | * Created by rnorth on 03/01/2016. 8 | */ 9 | public interface MailSenderService extends Callable { 10 | 11 | /** 12 | * Enqueue a message to be sent on a subsequent {@link DisqueBackedMailSenderService#doScheduledSend()} 13 | * @param message mail message to be sent 14 | * @return an ID for the enqueued message 15 | */ 16 | String enqueueMessage(MailMessage message); 17 | 18 | /** 19 | * Trigger a scheduled send of mail messages. 20 | * 21 | * TODO: In a real implementation this could be called by a {@link java.util.concurrent.ScheduledExecutorService}, 22 | * probably 23 | * 24 | * @return the result of sending queued messages. 25 | */ 26 | MailSenderService.Result doScheduledSend(); 27 | 28 | /** 29 | * call() implementation to allow MailSenderService to be used as a Callable. Simply delegates to 30 | * the {@link DisqueBackedMailSenderService#doScheduledSend()} method. 31 | * 32 | * @return the result of sending queued messages. 33 | * @throws Exception 34 | */ 35 | default MailSenderService.Result call() throws Exception { 36 | return doScheduledSend(); 37 | } 38 | 39 | @Data 40 | @RequiredArgsConstructor 41 | class Result { 42 | public final int successfulCount; 43 | public final int failedCount; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /linked-container/src/test/java/com/example/linkedcontainer/RedmineClientTest.java: -------------------------------------------------------------------------------- 1 | package com.example.linkedcontainer; 2 | 3 | import org.junit.Rule; 4 | import org.junit.Test; 5 | import org.junit.rules.RuleChain; 6 | import org.testcontainers.containers.PostgreSQLContainer; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | 10 | /** 11 | * Tests for RedmineClient. 12 | */ 13 | public class RedmineClientTest { 14 | 15 | private static final String POSTGRES_USERNAME = "redmine"; 16 | private static final String POSTGRES_PASSWORD = "secret"; 17 | 18 | private PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer("postgres:9.6.2") 19 | .withUsername(POSTGRES_USERNAME) 20 | .withPassword(POSTGRES_PASSWORD); 21 | 22 | private RedmineContainer redmineContainer = new RedmineContainer("redmine:3.3.2") 23 | .withLinkToContainer(postgreSQLContainer, "postgres") 24 | .withEnv("POSTGRES_ENV_POSTGRES_USER", POSTGRES_USERNAME) 25 | .withEnv("POSTGRES_ENV_POSTGRES_PASSWORD", POSTGRES_PASSWORD); 26 | 27 | @Rule 28 | public RuleChain chain = RuleChain.outerRule(postgreSQLContainer) 29 | .around(redmineContainer); 30 | 31 | @Test 32 | public void canGetIssueCount() throws Exception { 33 | RedmineClient redmineClient = new RedmineClient( 34 | redmineContainer.getRedmineUrl()); 35 | 36 | assertEquals( 37 | "The issue count can be retrieved.", 38 | 0, 39 | redmineClient.getIssueCount()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /spring-boot/src/test/java/com/example/AbstractIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import org.junit.ClassRule; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; 7 | import org.springframework.boot.test.util.TestPropertyValues; 8 | import org.springframework.context.ApplicationContextInitializer; 9 | import org.springframework.context.ConfigurableApplicationContext; 10 | import org.springframework.test.context.ContextConfiguration; 11 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 12 | import org.testcontainers.containers.GenericContainer; 13 | 14 | @RunWith(SpringJUnit4ClassRunner.class) 15 | @SpringBootTest(classes = DemoApplication.class,webEnvironment = WebEnvironment.RANDOM_PORT) 16 | @ContextConfiguration(initializers = AbstractIntegrationTest.Initializer.class) 17 | public abstract class AbstractIntegrationTest { 18 | 19 | @ClassRule 20 | public static GenericContainer redis = new GenericContainer("redis:3.0.6").withExposedPorts(6379); 21 | 22 | public static class Initializer implements ApplicationContextInitializer { 23 | @Override 24 | public void initialize(ConfigurableApplicationContext configurableApplicationContext) { 25 | TestPropertyValues values = TestPropertyValues.of( 26 | "spring.redis.host=" + redis.getContainerIpAddress(), 27 | "spring.redis.port=" + redis.getMappedPort(6379) 28 | ); 29 | values.applyTo(configurableApplicationContext); 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /disque-job-queue/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | testcontainers-java-examples 7 | org.testcontainers.examples 8 | NOVERSION 9 | 10 | 4.0.0 11 | 12 | disque-job-queue 13 | 14 | 15 | biz.paluch.redis 16 | spinach 17 | 0.2 18 | 19 | 20 | com.google.code.gson 21 | gson 22 | 2.5 23 | 24 | 25 | com.google.guava 26 | guava 27 | 18.0 28 | 29 | 30 | org.slf4j 31 | slf4j-api 32 | 1.7.7 33 | provided 34 | 35 | 36 | ch.qos.logback 37 | logback-classic 38 | 1.1.2 39 | test 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /redis-backed-cache/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | testcontainers-java-examples 7 | org.testcontainers.examples 8 | NOVERSION 9 | 10 | 4.0.0 11 | 12 | redis-backed-cache 13 | 14 | 15 | redis.clients 16 | jedis 17 | 2.8.0 18 | 19 | 20 | com.google.code.gson 21 | gson 22 | 2.5 23 | 24 | 25 | com.google.guava 26 | guava 27 | 18.0 28 | 29 | 30 | org.slf4j 31 | slf4j-api 32 | 1.7.7 33 | provided 34 | 35 | 36 | ch.qos.logback 37 | logback-classic 38 | 1.1.2 39 | test 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /selenium-container/src/test/java/SeleniumContainerTest.java: -------------------------------------------------------------------------------- 1 | import org.junit.Rule; 2 | import org.junit.Test; 3 | import org.openqa.selenium.WebElement; 4 | import org.openqa.selenium.remote.DesiredCapabilities; 5 | import org.openqa.selenium.remote.RemoteWebDriver; 6 | import org.testcontainers.containers.BrowserWebDriverContainer; 7 | 8 | import java.io.File; 9 | 10 | import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue; 11 | import static org.testcontainers.containers.BrowserWebDriverContainer.VncRecordingMode.RECORD_ALL; 12 | 13 | /** 14 | * Simple example of plain Selenium usage. 15 | */ 16 | public class SeleniumContainerTest { 17 | 18 | @Rule 19 | public BrowserWebDriverContainer chrome = new BrowserWebDriverContainer() 20 | .withDesiredCapabilities(DesiredCapabilities.chrome()) 21 | .withRecordingMode(RECORD_ALL, new File("target")); 22 | 23 | @Test 24 | public void simplePlainSeleniumTest() { 25 | RemoteWebDriver driver = chrome.getWebDriver(); 26 | 27 | driver.get("https://wikipedia.org"); 28 | WebElement searchInput = driver.findElementByName("search"); 29 | 30 | searchInput.sendKeys("Rick Astley"); 31 | searchInput.submit(); 32 | 33 | WebElement otherPage = driver.findElementByLinkText("Rickrolling"); 34 | otherPage.click(); 35 | 36 | boolean expectedTextFound = driver.findElementsByCssSelector("p") 37 | .stream() 38 | .anyMatch(element -> element.getText().contains("meme")); 39 | 40 | assertTrue("The word 'meme' is found on a page about rickrolling", expectedTextFound); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /redis-backed-cache/src/test/java/RedisBackedCacheTest.java: -------------------------------------------------------------------------------- 1 | import com.mycompany.cache.Cache; 2 | import com.mycompany.cache.RedisBackedCache; 3 | import org.junit.Before; 4 | import org.junit.Rule; 5 | import org.junit.Test; 6 | import org.testcontainers.containers.GenericContainer; 7 | import redis.clients.jedis.Jedis; 8 | 9 | import java.util.Optional; 10 | 11 | import static org.rnorth.visibleassertions.VisibleAssertions.*; 12 | 13 | /** 14 | * Integration test for Redis-backed cache implementation. 15 | */ 16 | public class RedisBackedCacheTest { 17 | 18 | @Rule 19 | public GenericContainer redis = new GenericContainer("redis:3.0.6") 20 | .withExposedPorts(6379); 21 | private Cache cache; 22 | 23 | @Before 24 | public void setUp() throws Exception { 25 | Jedis jedis = new Jedis(redis.getContainerIpAddress(), redis.getMappedPort(6379)); 26 | 27 | cache = new RedisBackedCache(jedis, "test"); 28 | } 29 | 30 | @Test 31 | public void testFindingAnInsertedValue() { 32 | cache.put("foo", "FOO"); 33 | Optional foundObject = cache.get("foo", String.class); 34 | 35 | assertTrue("When an object in the cache is retrieved, it can be found", 36 | foundObject.isPresent()); 37 | assertEquals("When we put a String in to the cache and retrieve it, the value is the same", 38 | "FOO", 39 | foundObject.get()); 40 | } 41 | 42 | @Test 43 | public void testNotFindingAValueThatWasNotInserted() { 44 | Optional foundObject = cache.get("bar", String.class); 45 | 46 | assertFalse("When an object that's not in the cache is retrieved, nothing is found", 47 | foundObject.isPresent()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /linked-container/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | testcontainers-java-examples 7 | org.testcontainers.examples 8 | NOVERSION 9 | 10 | 4.0.0 11 | 12 | linked-container 13 | 14 | 15 | 16 | ${testcontainers.group} 17 | postgresql 18 | ${testcontainers.version} 19 | 20 | 21 | org.postgresql 22 | postgresql 23 | 9.4.1212 24 | test 25 | 26 | 27 | org.slf4j 28 | slf4j-api 29 | 1.7.24 30 | provided 31 | 32 | 33 | ch.qos.logback 34 | logback-classic 35 | 1.2.1 36 | test 37 | 38 | 39 | com.squareup.okhttp3 40 | okhttp 41 | 3.6.0 42 | 43 | 44 | org.json 45 | json 46 | 20160810 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /spring-boot/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | testcontainers-java-examples 7 | org.testcontainers.examples 8 | NOVERSION 9 | 10 | 4.0.0 11 | 12 | spring-boot 13 | 14 | 15 | UTF-8 16 | 1.8 17 | 18 | 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-dependencies 24 | 2.0.1.RELEASE 25 | pom 26 | import 27 | 28 | 29 | 30 | 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-data-redis 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-web 39 | 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-starter-test 44 | test 45 | 46 | 47 | 48 | 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-maven-plugin 53 | 2.0.1.RELEASE 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /disque-job-queue/src/main/java/DisqueBackedMailSenderService.java: -------------------------------------------------------------------------------- 1 | import biz.paluch.spinach.DisqueClient; 2 | import biz.paluch.spinach.api.Job; 3 | import biz.paluch.spinach.api.sync.DisqueCommands; 4 | import com.google.gson.Gson; 5 | 6 | import java.util.HashSet; 7 | import java.util.List; 8 | import java.util.Set; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | /** 12 | * Simple example of how a mail service can be backed by a Disque job queue. 13 | * 14 | * It's assumed that for actual sending (SMTP, HTTP API, etc), this service calls a {@link MailApiClient}. 15 | * However, this can be expected to be quite unreliable, so use of Disque as a backing store helps keep track 16 | * of sent/failed state. 17 | */ 18 | public class DisqueBackedMailSenderService implements MailSenderService { 19 | 20 | private final DisqueCommands disque; 21 | private final Gson gson; 22 | private final MailApiClient mailApiClient; 23 | 24 | public DisqueBackedMailSenderService(DisqueClient client, MailApiClient mailApiClient) { 25 | this.mailApiClient = mailApiClient; 26 | 27 | // Obtain a Disque connection from DisqueClient 28 | disque = client.connect().sync(); 29 | 30 | gson = new Gson(); 31 | } 32 | 33 | @Override 34 | public String enqueueMessage(MailMessage message) { 35 | String messageAsJson = gson.toJson(message); 36 | 37 | // Enqueue the message as JSON, setting a TTL of 1 day (will expire off the queue if not sent by then) 38 | String jobId = disque.addjob("mail", messageAsJson, 1, TimeUnit.DAYS); 39 | 40 | return jobId; 41 | } 42 | 43 | @Override 44 | public Result doScheduledSend() { 45 | 46 | Set succeededJobIds = new HashSet<>(); 47 | Set failedJobIds = new HashSet<>(); 48 | 49 | // Wait up to 100ms for new messages to arrive on the queue 50 | // Retrieve up to 10 messages 51 | // (Ordinarily we'd make these configurable!) 52 | List> jobs = disque.getjobs(100, TimeUnit.MILLISECONDS, 10, "mail"); 53 | 54 | for (Job job : jobs) { 55 | String jsonBody = job.getBody(); 56 | MailMessage message = gson.fromJson(jsonBody, MailMessage.class); 57 | 58 | try { 59 | mailApiClient.send(message); 60 | succeededJobIds.add(job.getId()); 61 | } catch (MailApiClient.MailSendException e) { 62 | failedJobIds.add(job.getId()); 63 | } 64 | } 65 | 66 | // For any failed messages, proactively return to the queue 67 | disque.nack(failedJobIds.toArray(new String[failedJobIds.size()])); 68 | // For any successful jobs, mark as completed 69 | disque.ackjob(succeededJobIds.toArray(new String[succeededJobIds.size()])); 70 | 71 | return new Result(succeededJobIds.size(), failedJobIds.size()); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /disque-job-queue/src/test/java/SingleDisqueInstanceTest.java: -------------------------------------------------------------------------------- 1 | import biz.paluch.spinach.DisqueClient; 2 | import biz.paluch.spinach.DisqueURI; 3 | import biz.paluch.spinach.api.AddJobArgs; 4 | import biz.paluch.spinach.api.Job; 5 | import biz.paluch.spinach.api.sync.DisqueCommands; 6 | import org.junit.Before; 7 | import org.junit.Rule; 8 | import org.junit.Test; 9 | import org.testcontainers.containers.GenericContainer; 10 | 11 | import java.util.concurrent.TimeUnit; 12 | 13 | import static org.rnorth.visibleassertions.VisibleAssertions.*; 14 | 15 | /** 16 | * Created by rnorth on 03/01/2016. 17 | */ 18 | public class SingleDisqueInstanceTest { 19 | 20 | @Rule 21 | public GenericContainer container = new GenericContainer("richnorth/disque:1.0-rc1") 22 | .withExposedPorts(7711); 23 | private DisqueCommands connection; 24 | private AddJobArgs retryAfter1Second; 25 | 26 | @Before 27 | public void setup() { 28 | context(""); 29 | DisqueClient client = new DisqueClient(DisqueURI.create(container.getContainerIpAddress(), container.getMappedPort(7711))); 30 | connection = client.connect().sync(); 31 | retryAfter1Second = AddJobArgs.builder().retry(1, TimeUnit.SECONDS).build(); 32 | 33 | info("Initialized a fresh Disque instance and client"); 34 | } 35 | 36 | @Test 37 | public void testJobStoreAndRetrieve() { 38 | 39 | info("Adding a job to the queue"); 40 | connection.addjob("main_queue", "body", 1, TimeUnit.MINUTES); 41 | 42 | info("Getting a job from the queue"); 43 | Job job = connection.getjob("main_queue"); 44 | 45 | assertEquals("The retrieved job is the same as the one that was added", 46 | "body", 47 | job.getBody()); 48 | 49 | info("Acknowledging the job to mark completion"); 50 | connection.ackjob(job.getId()); 51 | } 52 | 53 | @Test 54 | public void testFailureToAckJobBeforeTimeout() throws InterruptedException { 55 | 56 | info("Adding a job to the queue"); 57 | connection.addjob("main_queue", "body", 1, TimeUnit.MINUTES, retryAfter1Second); 58 | 59 | info("Getting a job from the queue"); 60 | Job job = connection.getjob("main_queue"); 61 | 62 | assertEquals("The retrieved job is the same as the one that was added", 63 | "body", 64 | job.getBody()); 65 | 66 | info("Simulating a failure to ack the job before the timeout (1s)"); 67 | TimeUnit.SECONDS.sleep(2); 68 | 69 | info("Attempting to get another job from the queue - the RETRY setting for the job means it should have reappeared"); 70 | // The timeout specified here is how long the command will wait for a job to appear 71 | Job job2 = connection.getjob(5, TimeUnit.SECONDS, "main_queue"); 72 | 73 | assertNotNull("After re-getting the original job is back on the queue", job2); 74 | assertEquals("The retrieved job is the same as the one that was added", 75 | "body", 76 | job2.getBody()); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.testcontainers.examples 6 | testcontainers-java-examples 7 | NOVERSION 8 | 9 | redis-backed-cache 10 | disque-job-queue 11 | selenium-container 12 | spring-boot 13 | linked-container 14 | 15 | 16 | pom 17 | 18 | 19 | 20 | ${testcontainers.group} 21 | testcontainers 22 | ${testcontainers.version} 23 | 24 | 25 | 26 | junit 27 | junit 28 | 4.12 29 | provided 30 | 31 | 32 | 33 | 34 | org.projectlombok 35 | lombok 36 | 1.16.6 37 | provided 38 | 39 | 40 | 41 | 42 | org.rnorth.visible-assertions 43 | visible-assertions 44 | 1.0.5 45 | test 46 | 47 | 48 | org.mockito 49 | mockito-all 50 | 1.9.5 51 | test 52 | 53 | 54 | 55 | 56 | 57 | 58 | maven-compiler-plugin 59 | 3.1 60 | 61 | 1.8 62 | 1.8 63 | 64 | 65 | 66 | 67 | 68 | 69 | UTF-8 70 | com.github.testcontainers.testcontainers-java 71 | -SNAPSHOT 72 | 73 | 74 | 75 | 76 | 77 | 78 | Windows 79 | 80 | 81 | 82 | windows-support-SNAPSHOT 83 | 84 | 85 | 86 | 87 | 88 | 89 | jitpack.io 90 | https://jitpack.io 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /disque-job-queue/src/test/java/DisqueBackedMailSenderTest.java: -------------------------------------------------------------------------------- 1 | import biz.paluch.spinach.DisqueClient; 2 | import biz.paluch.spinach.DisqueURI; 3 | import org.junit.Before; 4 | import org.junit.Rule; 5 | import org.junit.Test; 6 | import org.testcontainers.containers.GenericContainer; 7 | 8 | import static org.mockito.Matchers.eq; 9 | import static org.mockito.Mockito.*; 10 | import static org.rnorth.visibleassertions.VisibleAssertions.*; 11 | 12 | /** 13 | * Created by rnorth on 03/01/2016. 14 | */ 15 | public class DisqueBackedMailSenderTest { 16 | 17 | @Rule 18 | public GenericContainer container = new GenericContainer("richnorth/disque:1.0-rc1") 19 | .withExposedPorts(7711); 20 | private DisqueClient disqueClient; 21 | 22 | private MailSenderService service; 23 | private MailApiClient mockMailApiClient; 24 | 25 | private MailMessage dummyMessage1; 26 | private MailMessage dummyMessage2; 27 | private MailMessage dummyMessage3; 28 | 29 | @Before 30 | public void setup() { 31 | context(""); 32 | disqueClient = new DisqueClient(DisqueURI.create(container.getContainerIpAddress(), container.getMappedPort(7711))); 33 | mockMailApiClient = mock(MailApiClient.class); 34 | 35 | service = new DisqueBackedMailSenderService(disqueClient, mockMailApiClient); 36 | 37 | info("Initialized a fresh Disque instance and service instance"); 38 | 39 | dummyMessage1 = new MailMessage("Dummy Message 1", "bob@example.com", "Test body"); 40 | dummyMessage2 = new MailMessage("Dummy Message 2", "bob@example.com", "Test body"); 41 | dummyMessage3 = new MailMessage("Dummy Message 3", "bob@example.com", "Test body"); 42 | } 43 | 44 | @Test 45 | public void testSimpleSuccessfulSending() throws Exception { 46 | service.enqueueMessage(dummyMessage1); 47 | service.enqueueMessage(dummyMessage2); 48 | service.enqueueMessage(dummyMessage3); 49 | 50 | when(mockMailApiClient.send(any(MailMessage.class))).thenReturn(true); 51 | 52 | service.doScheduledSend(); 53 | 54 | verify(mockMailApiClient).send(eq(dummyMessage1)); 55 | verify(mockMailApiClient).send(eq(dummyMessage2)); 56 | verify(mockMailApiClient).send(eq(dummyMessage3)); 57 | } 58 | 59 | @Test 60 | public void testRetryOnFailure() throws Exception { 61 | service.enqueueMessage(dummyMessage1); 62 | service.enqueueMessage(dummyMessage2); 63 | service.enqueueMessage(dummyMessage3); 64 | 65 | info("Message 1 will fail to send on the first attempt"); 66 | when(mockMailApiClient.send(eq(dummyMessage1))).thenThrow(MailApiClient.MailSendException.class).thenReturn(true); 67 | when(mockMailApiClient.send(eq(dummyMessage2))).thenReturn(true); 68 | when(mockMailApiClient.send(eq(dummyMessage3))).thenReturn(true); 69 | 70 | MailSenderService.Result result; 71 | context("Simulating sending messages"); 72 | context("First sending attempt", 4); 73 | result = service.doScheduledSend(); 74 | assertEquals("2 messages were 'sent' successfully", 2, result.successfulCount); 75 | assertEquals("1 messages failed", 1, result.failedCount); 76 | 77 | context("Second attempt", 4); 78 | result = service.doScheduledSend(); 79 | assertEquals("1 message was 'sent' successfully", 1, result.successfulCount); 80 | assertEquals("0 messages failed", 0, result.failedCount); 81 | 82 | context("Third attempt", 4); 83 | info("No messages should be due to send this time"); 84 | result = service.doScheduledSend(); 85 | assertEquals("0 messages were 'sent' successfully", 0, result.successfulCount); 86 | assertEquals("0 messages failed", 0, result.failedCount); 87 | 88 | verify(mockMailApiClient, times(2)).send(eq(dummyMessage1)); 89 | verify(mockMailApiClient).send(eq(dummyMessage2)); 90 | verify(mockMailApiClient).send(eq(dummyMessage3)); 91 | } 92 | } 93 | --------------------------------------------------------------------------------