Stoplight Prism

Mock Testing with Stoplight Prism

card

Introduction

Testing plays a crucial role in the software development process. Identifying issues before deployment rather than in production provides a significant advantage for code quality and API reliability.

However, there are instances where testing third-party services becomes either complex or unnecessary. This is where mocking becomes invaluable. Mocking allows you to focus on testing your core business logic without poking into the sophistication of other service behaviours.

What is Prism?

Prism is an open-source HTTP mock server that can emulate your API's behavior as if you already built it. Mock HTTP servers are generated from your OpenAPI v2/v3 documents.

In simple terms, Prism enables you to deploy an HTTP server within a Docker container, responding to predefined API calls based on specifications outlined in a YAML file. This streamlined approach significantly simplifies testing, allowing you to focus on your core business logic without unnecessary complexities.

Architecture

Let's get hands-on and build something while ensuring it's thoroughly tested. Imagine we have a service that a customer calls to create a user. We can explore a couple of potential endpoints:

go
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func main() {
// Create a new Gin router
router := gin.Default()
// Define routes
router.GET("/users", getAllUsers)
router.POST("/users", createUser)
router.GET("/users/:userId", getUserByID)
router.PUT("/users/:userId", updateUserByID)
router.DELETE("/users/:userId", deleteUserByID)
// Start the HTTP server on port 8080
err := router.Run(":8080")
if err != nil {
panic("Error starting the server: " + err.Error())
}
}

These endpoints are self-explanatory, constituting a straightforward REST API. Now, let's dive into the implementation of the createUser function.

go
// Handler function to create a new user
func createUser(c *gin.Context) {
// Decode the JSON request body into a User struct
var newUser User
if err := c.ShouldBindJSON(&newUser); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
// Validate user data
if newUser.Name == "" || newUser.Email == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user data"})
return
}
// Assign a unique ID and add the user to the list
newUser.ID = len(users) + 1
users = append(users, newUser)
err := superImportantService.SendUserDetails(newUser)
if err != nil {
c.JSON(http.StatusInternalServerError, "Error connecting to Super Important Service")
return
}
// Respond with the created user
c.JSON(http.StatusCreated, newUser)
}

This function is quite straightforward. We receive user data, validate that it's not empty, save the user to our database, send the user to superImportantService, and return a 201 status code.

Hold on a moment! What's this superImportantService?

Well, it's our third-party service where we need to store the user for a super important purpose.

go
type SuperImportantService struct {
EndpointURL string
}
// NewSuperImportantService creates a new instance of SuperImportantService
func NewSuperImportantService() *SuperImportantService {
endpointURL := os.Getenv("SUPER_IMPORTANT_SERVICE_URL") // "api.super-important-service.com"
return &SuperImportantService{EndpointURL: endpointURL}
}

Overall, the architecture looks like this:

architecture

The question arises when we want to test our service independently of the Super Important Service. Since it's not accessible from our local environment, tests will consistently fail.

Mocking Service

Here comes Prism! We can effortlessly create an entire Mock API Server using the OpenAPI specification. It's incredibly simple yet powerful.

While I won't provide the entire specification, here's an example of what it can look like:

yaml
openapi: 3.0.0
info:
title: User API
version: 1.0.0
paths:
/users:
post:
summary: Create a new user
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
email:
type: string
required:
- name
- email
responses:
'201':
description: User created successfully
content:
application/json:
schema:
type: object
properties:
id:
type: integer
name:
type: string
email:
type: string
example:
id: 3
name: New User
email: new.user@example.com

Let's take a look at it. We have information about our API, including the version and title. Following that are the paths, specifically the POST request for /users. The API requires a name and email and returns a 201 response code. While in real-life scenarios, we might encounter various response codes, for our small example project this simplification is enough.

Now, let's run it!

shell
npm install -g @stoplight/prism-cli
docker run --init --rm -v $(pwd):/tmp -p 4010:4010 stoplight/prism:5.5.1 mock -h 0.0.0.0 "/tmp/user-api-openapi.yaml"

Yes, it's that simple. If you've done everything correctly, you'll see a list of possible API endpoints and the URL of a Prism server. Now, you can set it up in your environment variables and test your application. You can even take it a step further and run test containers from Go tests! However, that's a topic for next time.

shell
[6:48:38 AM][CLI] … awaiting Starting Prism…
[6:48:40 AM][CLI] ℹ info GET http://0.0.0.0:4010/users
[6:48:40 AM][CLI] ℹ info POST http://0.0.0.0:4010/users
[6:48:40 AM][CLI] ℹ info GET http://0.0.0.0:4010/users/858
[6:48:40 AM][CLI] ℹ info PUT http://0.0.0.0:4010/users/76
[6:48:40 AM][CLI] ℹ info DELETE http://0.0.0.0:4010/users/31
[6:48:40 AM][CLI] ▶ start Prism is listening on http://0.0.0.0:4010

Here is a potential test function:

go
func TestCreateUser(t *testing.T) {
tests := []struct {
name string
user User
expCode int
}{
// TODO: define test cases
}
// Iterate through test cases
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("SUPER_IMPORTANT_SERVICE_URL", "http://0.0.0.0:4010")
// TODO: run test cases
})
}
}

Conclusion

In summary, Prism simplifies API testing by offering a smooth experience. It enables you to refine your code without the complexities of third-party services. Whether you're configuring Prism for local tests or exploring your function's behaviour without external dependencies, it proves to be a valuable addition to your testing toolkit. Embrace Prism for smoother coding experiences!