Logo

dev-resources.site

for different kinds of informations.

Fastify Meets WireMock: External Service Mocking

Published at
2/13/2024
Categories
fastify
typescript
wiremock
mock
Author
massimobiagioli
Categories
4 categories in total
fastify
open
typescript
open
wiremock
open
mock
open
Author
15 person written this
massimobiagioli
open
Fastify Meets WireMock: External Service Mocking

WireMock Wonders: Boosting Fastify Testability

This article reveals how to integrate WireMock into Fastify with ease, enabling developers to effortlessly generate mock responses for external services. Join us as we explore the straightforward process of seamlessly integrating and optimizing Fastify applications using WireMock for enhanced testing capabilities.

A Use Case: Integration with an External Service

In this scenario, we'll illustrate the integration with an external service. The service in focus simulates calls to a generic "devices" management system. Our objective is to retrieve the list of devices, read a device by its ID, and create a new one. Let's delve into the practical application of WireMock within Fastify for this specific use case.

Fastify App

We kick off by creating a simple Fastify application in TypeScript, utilizing the fastify-cli.

src/app.ts

export interface AppOptions extends FastifyServerOptions, Partial<AutoloadPluginOptions> {
}

const options: AppOptions = {
}

const FastifyEnvOpts = {
  dotenv: true,
  schema: {
    type: 'object',
    required: ['DEVICE_SERVER_URL'],
    properties: {
      DEVICE_SERVER_URL: {
        type: 'string'
      }
    }
  }
}

const app: FastifyPluginAsync<AppOptions> = async (
  fastify,
  opts
): Promise<void> => {
  void fastify.register(FastifyEnv, FastifyEnvOpts)

  void fastify.register(AutoLoad, {
    dir: join(__dirname, 'plugins'),
    options: opts
  })

  void fastify.register(AutoLoad, {
    dir: join(__dirname, 'routes'),
    options: opts
  })
}

export default app
export { app, options }
Enter fullscreen mode Exit fullscreen mode

Building a Service to Interact with the External System

Now, let's craft a service that interacts with the external system. In this section, we'll guide you through the process of creating a Fastify service that communicates with the simulated external 'devices' management system. This hands-on approach will showcase the seamless integration of our Fastify application with WireMock for effective testing and interaction with external services.

src/plugins/deviceService.ts

// ... imports

export default fp(async (fastify, opts) => {
  const SERVER_BASE_URL = `${fastify.config.DEVICE_SERVER_URL}/api/v1/device`

  const deviceService = {
    getDevices: async (): Promise<Device[]> => {
      const response = await fetch(SERVER_BASE_URL)
      return await response.json() as Device[]
    },
    getDeviceById: async (id: string): Promise<Device | null> => {
      const response = await fetch(`${SERVER_BASE_URL}/${id}`)
      if (response.status === 404) {
        return null
      }
      return await response.json() as Device
    },
    createDevice: async (device: DeviceRequest): Promise<Device> => {
      const response = await fetch(SERVER_BASE_URL, {
        method: 'POST',
        body: JSON.stringify(device),
        headers: {
          'Content-Type': 'application/json'
        }
      })
      return await (await response.json() as Promise<Device>)
    }
  }

  fastify.decorate('deviceService', deviceService)
})
Enter fullscreen mode Exit fullscreen mode

src/model/device.ts

export interface DeviceRequest {
  name: string
  type: string
  address: string
}

export type Device = DeviceRequest & {
  id: string
}
Enter fullscreen mode Exit fullscreen mode

Using the deviceService in the Controller

src/routes/device/index.ts

// ...imports

const route: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
  fastify.get<{ Reply: Device[] }>('/', async function (request, reply) {
    return await fastify.deviceService.getDevices()
  })

  fastify.get<{ Params: GetDeviceByIdParams, Reply: Device | GetDeviceByIdError }>('/:id', async function (request, reply) {
    const result = await fastify.deviceService.getDeviceById(request.params.id)
    if (result === null) {
      return await reply.notFound()
    }
    return result
  })

  fastify.post<{ Body: DeviceRequest, Reply: Device }>('/', async function (request, reply) {
    const result = await fastify.deviceService.createDevice(request.body)
    return reply.code(201).send(result)
  })
}

export default route
Enter fullscreen mode Exit fullscreen mode

Setting Up WireMock

Let's dive into configuring WireMock for our project. We'll employ Docker to define the WireMock service, simplifying the setup process. By using containers, we ensure a convenient and reproducible environment for running WireMock alongside our Fastify application.

docker-compose.yml

version: '3.9'
services:
  wiremock:
    image: wiremock/wiremock:3.3.1
    restart: always
    ports:
      - '8080:8080'
    volumes:
      - ./wiremock_data:/home/wiremock
    entrypoint: ["/docker-entrypoint.sh", "--global-response-templating", "--verbose"]
Enter fullscreen mode Exit fullscreen mode

wiremock_data/mappings/get_devices.json

{
  "request" : {
    "url" : "/api/v1/device",
    "method" : "GET"
  },
  "response" : {
    "status" : 200,
    "bodyFileName" : "get_devices_response.json",
    "headers" : {
      "Content-Type" : "application/json"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

wiremock_data/__files/get_devices_response.json

[
  {
    "id": 1,
    "name": "First Device",
    "family_id": 1,
    "address": "10.0.1.1"
  },
  ...
]
Enter fullscreen mode Exit fullscreen mode

Image description

wiremock_data/mappings/get_device_by_id.json

{
  "request" : {
    "urlPattern": "^/api/v1/device/\\d*",
    "method" : "GET"
  },
  "response" : {
    "status" : 200,
    "bodyFileName" : "get_device_by_id_response.json",
    "headers" : {
      "Content-Type" : "application/json"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

wiremock_data/__files/get_device_by_id_response.json

This template utilizes dynamic templating, generating a response tailored to the input parameters. In this instance, the 'id' parameter influences the response, showcasing the flexibility and adaptability achieved through WireMock's templating capabilities.

{
  "id": {{ request.pathSegments.[3] }},
  "name": "Device {{ request.pathSegments.[3] }}",
  "family_id": 1,
  "address": "10.0.1.{{ request.pathSegments.[3] }}"
}
Enter fullscreen mode Exit fullscreen mode

Image description

wiremock_data/mappings/get_device_by_id_404.json

{
  "request" : {
    "urlPattern": "^/api/v1/device/\\d*404$",
    "method" : "GET"
  },
  "response" : {
    "status" : 404,
    "body" : ""
  }
}
Enter fullscreen mode Exit fullscreen mode

The setup is designed to manage scenarios where the 'id' ends with '404'. When a request matches the defined URL pattern and method (GET), WireMock responds with a 404 status code, providing a straightforward mechanism to simulate and test error conditions for our Fastify application.

Image description

wiremock_data/mappings/create_device.json

{
  "request" : {
    "url" : "/api/v1/device",
    "method" : "POST",
    "headers" : {
      "Content-Type": {
        "equalTo": "application/json"
      }
    },
    "bodyPatterns": [
      {
        "matchesJsonPath": "$.name"
      },
      {
        "matchesJsonPath": "$.family_id"
      },
      {
        "matchesJsonPath": "$.address"
      }
    ]
  },
  "response" : {
    "status" : 201,
    "bodyFileName" : "create_device_response.json",
    "headers" : {
      "Content-Type" : "application/json"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

wiremock_data/__files/create_device_response.json

{
  "id": {{randomValue type='NUMERIC' length=5}},
  "name": "{{jsonPath request.body '$.name'}}",
  "family_id": {{jsonPath request.body '$.family_id'}},
  "address": "{{jsonPath request.body '$.address'}}"
}
Enter fullscreen mode Exit fullscreen mode

We leverage dynamic templating to generate varied responses. The 'id' field is populated with a random numeric value of length 5. The 'name', 'family_id', and 'address' fields are populated based on the corresponding values present in the incoming request body. This dynamic approach allows us to simulate diverse scenarios and handle input data flexibly within our WireMock setup.

Image description

Testing

During the testing phase, we don't need to perform service-level mocks since we are directing our requests to the WireMock URL. By utilizing WireMock as the target URL, we seamlessly integrate our Fastify application with the WireMock service, allowing for realistic simulations and comprehensive testing scenarios without the need for extensive service-level mocking.

test/routes/device.test.ts

...

test('get device by id', async (t) => {
    const app = await build(t)

    const res = await app.inject({
        url: '/device/123'
    })

    const payload = JSON.parse(res.payload)
    assert.equal(res.statusCode, 200)
    assert.deepStrictEqual(payload, { id: 123, name: 'Device 123', family_id: 1, address: '10.0.1.123' })
})

test('get device by id - 404', async (t) => {
    const app = await build(t)

    const res = await app.inject({
        url: '/device/1404'
    })

    const payload = JSON.parse(res.payload)
    assert.equal(res.statusCode, 404)
    assert.deepStrictEqual(payload, { "statusCode": 404, "error": "Not Found", "message": "Not Found" })
})

...
Enter fullscreen mode Exit fullscreen mode

Conclusions

WireMock provides a comprehensive solution for building a test suite towards external services, eliminating the need for patching services at the application level. This approach enables us to conduct real tests, fostering a robust testing environment and ensuring the reliability and effectiveness of our Fastify application in interacting with external services.
The project code is in this GitHub repository: fastify-wiremock-example.

mock Article's
30 articles in total
Favicon
FAQ β€” Bloomer Mock Data Generator
Favicon
Best Practices: How to Make Your API Smarter and More Flexible
Favicon
Testing ReactJS Context - A Guide with test-doubles
Favicon
The beauty of MSW
Favicon
Realistic data Generation Using Bloomer Mock
Favicon
Testing Spring Boot Applications: Unit, Integration, and Mocking β€” A Comprehensive Guide
Favicon
How to debug your Python mocks or imports
Favicon
Is there any option for Mock EncryptAsync() in Azure.Security.KeyVault.Keys.Cryptography
Favicon
Learn to Simulate Online APIs Without Server Infrastructure
Favicon
Diferenças entre o jest.spyOn e jest.mock
Favicon
Mock Class Constructor in Jest Test with Mocking Partials
Favicon
You stub/mock incorrectly
Favicon
Step-by-Step Tutorial on Setting Up a Mock Server Using Postman
Favicon
Mocking ES6 Class Instances with Jest
Favicon
Create free mock apis with unlimited storage
Favicon
Free API server with unlimited access
Favicon
Handle Variables and Dynamic Values with `jest.fn()`
Favicon
A way to mock PHP internal functions with xepozz/internal-mocker
Favicon
Mock vs Stub vs Fake: Understand the difference
Favicon
Mock modules properly with Jest and Typescript
Favicon
Simplified Strategies for Mocking API Calls
Favicon
Fastify Meets WireMock: External Service Mocking
Favicon
OpenAI API Mock for Devs on a Budget
Favicon
Mock S3 for AWS SDK for JavaScript (v3)
Favicon
Simplifying Kafka Testing in Python: A Mockafka-py Tutorial
Favicon
When to use Mock API?
Favicon
Datafaker: Simplifying Test Data Generation for Java and Kotlin
Favicon
NestJS: Mocking Databases for Efficient Tests
Favicon
Understanding Mocks, Stubs, and Fakes in Software Testing
Favicon
How to Typescript to JSON with Butlermock

Featured ones: