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.
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.
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:
gotype User struct {ID int `json:"id"`Name string `json:"name"`Email string `json:"email"`}func main() {// Create a new Gin routerrouter := gin.Default()// Define routesrouter.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 8080err := 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 userfunc createUser(c *gin.Context) {// Decode the JSON request body into a User structvar newUser Userif err := c.ShouldBindJSON(&newUser); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})return}// Validate user dataif 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 listnewUser.ID = len(users) + 1users = 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 userc.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.
gotype SuperImportantService struct {EndpointURL string}// NewSuperImportantService creates a new instance of SuperImportantServicefunc NewSuperImportantService() *SuperImportantService {endpointURL := os.Getenv("SUPER_IMPORTANT_SERVICE_URL") // "api.super-important-service.com"return &SuperImportantService{EndpointURL: endpointURL}}
Overall, the architecture looks like this:
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.
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:
yamlopenapi: 3.0.0info:title: User APIversion: 1.0.0paths:/users:post:summary: Create a new userrequestBody:required: truecontent:application/json:schema:type: objectproperties:name:type: stringemail:type: stringrequired:- nameresponses:'201':description: User created successfullycontent:application/json:schema:type: objectproperties:id:type: integername:type: stringemail:type: stringexample:id: 3name: New Useremail: 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!
shellnpm install -g @stoplight/prism-clidocker 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:
gofunc TestCreateUser(t *testing.T) {tests := []struct {name stringuser UserexpCode int}{// TODO: define test cases}// Iterate through test casesfor _, 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})}}
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!