Mock Testing with Stoplight Prism

- By Dmytro Veretelnyk
Blog post image

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:

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.

// 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.

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:

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!

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.

[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:

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!

About Dmytro Veretelnyk

Backend Engineer at Kablamo