Service stub met behulp van Pact

Voor het lokaal testen van een applicatie is het soms handig, of noodzakelijk, om externe services waar je applicatie gebruik van maakt te mocken of te stubben. Zodat een applicatie geïsoleerd getest kan worden zonder dat dit invloed heeft op de toestand van eventuele afhankelijke test- of productie-applicaties.

Met als bijkomend voordeel dat het dan ook niet nodig is een instantie van de betreffende externe services lokaal te deployen.

Op zoek naar een tool om snel en eenvoudig een stub op te zetten, blijkt Pact prettig gereedschap.

Ervaring met Pact

Pact.io is een testtool voor het testen van het contract tussen een consumer en een provider van een service. Voor verdere details daarover zie evt ook https://docs.pact.io/.

In deze blog ga ik in op een klein deel van Pact: De server stub. En dan met name hoe je deze opzet en hoe je de stub inzet bij integratietests.

Een service stub maken

Pact biedt een Docker container die aan de hand van voorgedefinieerde interacties een service kan starten die deze interacties naspeelt. Een interactie is een combinatie van een request en een response. Deze worden in een pact-bestand opgeslagen.

Iedere request die deze service ontvangt wordt geprobeerd te matchen tegen een request in het pact bestand. Indien er een match is, volgt de bijbehorende response.

stub

Hieronder een voorbeeld van een pact-bestand waarin twee interacties met de volgende requestpaden zijn gedefinieerd:

/health: een health check
/products: een rest endpoint voor het ophalen van een lijst producten

{
  "consumer": {
    "name": "some-consumer"
  },
  "provider": {
    "name": "some-provider"
  },
  "interactions": [
    {
      "description": "health check",
      "provider_state": "Some state",
      "request": {
        "method": "GET",
        "path": "/health"
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": "text/plain"
        },
        "body": "OK"
      }
    },
    {
      "description": "find all rpoducts",
      "provider_state": "Some state",
      "request": {
        "method": "GET",
        "path": "/products"
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": "application/json"
        },
        "body":
        {
          "Products": {
            "Product": [
              {
                "CatalogueID": "101",
                "Name": "Widget",
                "Price": "10.99",
                "Manufacturer": "Company A",
                "InStock": "Yes"
              },
              {
                "CatalogueID": "300",
                "Name": "Fooble",
                "Price": "2.00",
                "Manufacturer": "Company B",
                "InStock": "No"
              }
            ]
          }
		}
      }
    }
  ]
}

De service stub heeft alleen dit bestand nodig om een service te starten. De volgende Dockerfile gebruikt het pact bestand en kopieert dit bestand genaamd pact.json in de Docker container.
FROM pactfoundation/pact-stub-server:latest
COPY pact.json  /app/pacts/
ENTRYPOINT ["/app/pact-stub-server", "-d=/app/pacts", "-p=8080"]
EXPOSE 8080

Nu hoeven we alleen nog onze eigen variant van de Docker container te bouwen en deze te starten:
$ docker build -t provider-stub .
$ docker run -p 8080:8080 provider-stub

Zodra de container gestart is, controleer je of de stub het doet door in een browser de url http://localhost:8080/products  te benaderen. Je moet dan een lijstje van producten terugkrijgen dat in de pact file is gedefinieerd.

Integratietest met deze stub

Hoe je de server stub inzet voor het uitvoeren van integratietests is goed te demonstreren met een aantal code snippets uit een werkende applicatie. Deze applicatie noemen we voor het gemak de consumer. Het is een Spring Boot-applicatie geschreven in Kotlin, gebouwd in Maven.

De consumer is degene die de provider gaat bevragen.

De Fabric8-maven-plugin maakt het mogelijk om vooraf aan het uitvoeren van de integratietest tijdelijk een Docker container te starten. In dit voorbeeld bepaalt Docker zelf op welke poort de provider stub beschikbaar komt. Dit in tegenstelling tot het eerdergenoemde Docker run commando waar de interne port gemapt wordt naar port 8080.

De Docker-maven-plugin voert vervolgens in de pom.xml de volgende stappen uit:

  • Bouw Docker container voor de stub volgens het recept in de opgegeven Dockerfile.
  • Start container. De poort waarop de stub te benaderen is, wordt toegekend aan de property provider-stub.port. De maven failsafe-plugin maakt deze properties beschikbaar als environment variabele zodat deze later in de consumer klaarstaan om de client te configureren.
  • Wacht tot de health check van de stub een status code 200 retourneert. Na het uitvoeren van de integratietest wordt de container van de stub weer gestopt.

<project>
  ...
  <properties>
    <docker.fabric8.docker-maven-plugin>0.26.0</docker.fabric8.docker-maven-plugin>
    <dns.host.docker>localhost</dns.host.docker>
    <!-- Default port waarop stub beschikbaar is-->
    <provider-stub.port>8081</provider-stub.port>
  </properties>
  ...
  <build>
    <plugins>
      ...
            <!-- integration testing-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-failsafe-plugin</artifactId>
                <executions>
                    <execution>
                        <id>failsafe-integration-test</id>
                        <phase>integration-test</phase>
                        <goals>
                            <goal>integration-test</goal>
                            <goal>verify</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                	<!-- Maak de volgende environment variabele beschikbaar voor de applicatie-->
                    <environmentVariables>
                        <catalog-service.port>${provider-stub.port}</catalog-service.port>
                        <dns.host.docker>${dns.host.docker}</dns.host.docker>
                    </environmentVariables>
                </configuration>
            </plugin>
            <plugin>
                <groupId>io.fabric8</groupId>
                <artifactId>docker-maven-plugin</artifactId>
                <version>${docker.fabric8.docker-maven-plugin}</version>
                <executions>
                    <execution>
                        <id>build</id>
                        <phase>pre-integration-test</phase>
                        <goals>
                            <goal>build</goal>
                            <goal>start</goal>
                        </goals>
                        <configuration>
                            <images>
                                <image>
                                    <name>provider-stub:0.0.1-it</name>
                                    <alias>provider-stub</alias>
                                    <build>
                                        <!-- verwijzing naar de Dockerfile van de pact stub -->
                                        <dockerFileDir>${project.basedir}/stub/</dockerFileDir>
                                        <tags>
                                            <tag>0.0.1-it</tag>
                                        </tags>
                                    </build>
                                    <run>
                                        <ports>
                                            <port>provider-stub.port:8080</port>
                                        </ports>
                                        <wait>
                                            <!-- preconditie voordat de integratie test uitgevoerd kan worden -->
                                            <http>
                                                <url>http://${dns.host.docker}:${provider-stub.port}/health</url>
                                                <method>GET</method>
                                                <status>200</status>
                                            </http>
                                            <time>5000</time>
                                        </wait>
                                    </run>
                                </image>
                            </images>
                        </configuration>
                    </execution>
                    <execution>
                        <id>remove-provider-stub</id>
                        <phase>post-integration-test</phase>
                        <goals>
                            <goal>stop</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
    </plugins>
  </build>
</project>

De consumer heeft een client die communiceert met de provider. De client class zal een dergelijke opzet hebben. Het endpoint van de provider (hieronder terug te vinden als variabele providerUrl) staat in een property.
@Service
class ProviderClient : IProvider {

    @Value("\${provider.url}")
    private lateinit var providerUrl: String

    override fun getProducts(): Set<Products> {
    	...
    }
}

De integratietest maakt gebruik van deze client om producten op te halen en hier eventueel in de test iets mee te doen.

Doordat de class naam eindigt op IT identificeert hij zich als integratietest en start de maven plugin de stub server voor het uitvoeren van de test. Tegelijkertijd laadt het profiel voor de juiste configuratie van de client, zodat deze met de stub gaat communiceren.

@SpringBootTest
@ActiveProfiles("integration-test")
class ProviderClientIT(){

	@Autowired
    private lateinit var providerClient: IProvider

    @Test
    fun getCapabilitiesTest() {
		val products =  providerClient.getProducts()
        ...
        ... // a lot of assertions
    }
}

De Spring Boot-configuratie bevat het profiel integration-test van bovenstaande integratietest. Hier zie je hoe de provider.url property geset wordt. Gevuld met de twee variabelen vanuit de maven-plugin.
spring:
  profiles: default
provider:
    url: http://localhost:8081/
---
spring:
  profiles: prod
provider:
    url: http://www.some-provider.com/
---
spring:
  profiles: integration-test
provider:
    url: http://${dns.host.docker}:${provider-stub.port}/
---

Conclusie

Pact biedt een eenvoudige manier om services te stubben. Dit kan handig zijn voor het testen van lokaal gedeployde applicaties die afhankelijkheden hebben met externe services waar mogelijk geen toegang toe is.

En het mooie is dat diezelfde stub voor het lokaal testen, ook gebruikt kan worden voor de integratie.

Leave a Reply

Your email address will not be published. Required fields are marked *