Learn how to create custom System-X services that integrate seamlessly with TNB’s testing framework. This guide covers both remote and self-hosted service implementations.
package software.tnb.myservice.service;import software.tnb.common.deployment.WithDockerImage;import software.tnb.common.service.Service;import software.tnb.myservice.account.MyServiceAccount;import software.tnb.myservice.validation.MyServiceValidation;public abstract class MyService extends Service<MyServiceAccount, MyServiceClient, MyServiceValidation> implements WithDockerImage { // Abstract methods that deployment-specific implementations must provide public abstract String hostname(); public abstract int port(); @Override public String defaultImage() { return "quay.io/myorg/myservice:1.0"; } protected MyServiceClient client() { if (client == null) { client = new MyServiceClient(hostname(), port()); } return client; } public MyServiceValidation validation() { if (validation == null) { validation = new MyServiceValidation(client()); } return validation; } public void openResources() { // Called after deployment to initialize clients client(); validation(); } public void closeResources() { // Called before undeployment to close connections if (client != null) { client.close(); client = null; } validation = null; }}
Step 2: Create the local (TestContainers) implementation
package software.tnb.myservice.resource.local;import software.tnb.common.deployment.ContainerDeployable;import software.tnb.myservice.service.MyService;import org.testcontainers.containers.GenericContainer;import org.testcontainers.containers.wait.strategy.Wait;import com.google.auto.service.AutoService;@AutoService(MyService.class)public class LocalMyService extends MyService implements ContainerDeployable<MyServiceContainer> { private static final Logger LOG = LoggerFactory.getLogger(LocalMyService.class); private MyServiceContainer container; @Override public void deploy() { LOG.info("Starting MyService container"); container = new MyServiceContainer(image()); container.start(); LOG.info("MyService container started"); } @Override public void undeploy() { if (container != null) { LOG.info("Stopping MyService container"); container.stop(); } } @Override public MyServiceContainer container() { return container; } @Override public String hostname() { return container.getHost(); } @Override public int port() { return container.getMappedPort(8080); }}
Create the container class:
package software.tnb.myservice.resource.local;import org.testcontainers.containers.GenericContainer;import org.testcontainers.containers.wait.strategy.Wait;public class MyServiceContainer extends GenericContainer<MyServiceContainer> { private static final int PORT = 8080; public MyServiceContainer(String image) { super(image); withExposedPorts(PORT); waitingFor(Wait.forHttp("/health").forStatusCode(200)); withEnv("SERVICE_MODE", "test"); } public int getServicePort() { return getMappedPort(PORT); }}
Create tests to verify your service implementation:
public class MyServiceTest { @RegisterExtension public static MyService service = ServiceFactory.create(MyService.class); @Test public void testLocalDeployment() { // Test runs with TestContainers by default assertNotNull(service.validation()); assertTrue(service.hostname().contains("localhost")); } @Test public void testServiceOperations() { String id = service.validation().createResource("test", Map.of()); assertNotNull(id); service.validation().deleteResource(id); }}