Building a REST API that plays nice with MongoDB is a common challenge in web development. But how do you make sure it all works seamlessly? That’s where integration testing comes in. In this blog post, we’re going to break down the process of writing integration tests for your REST API, specifically when MongoDB is in the mix.
You can get all of the code samples for this blog from this repository.
Simple Design of the API
As you can see, only component of our API is MongoDB, which is kind of not realistic for real life examples but you will get the idea
on how to apply for it for multiple components for integration tests.
Database Models For the API
Each author can have many books
Each book can have many comments.
Please do not try to validate the design of the models. It is just designed in a way where I can write the code fast and have the tests ready in short period of time.
API
Our api has 3 different endpoints.
GET /api/books: returns all of the books with their corresponding comments.
GET /api/author/{id}/books: returns the books of the author with given id.
POST /api/book: creates a new book.
You can check the example request and responses from the project readme.
How to Design Integration Tests
Let’s check our PostsController class which is basically handling all of the requests.
mongoRepository implements the Repository interface and, the only dependency for it is the mongo.Database.
In short terms, to be able to test our controller end2end, we need a MongoDB connection, but the real question is how to get a real MongoDB connection.
Test Containers
The answer is to use the Test-Containers. What is test-containers?
Testcontainers is an open source framework for providing throwaway, lightweight instances of databases, message brokers, web browsers, or just about anything that can run in a Docker container1.
So, here is our strategy for testing.
Run a MongoDB container with Test-Containers before doing the test.
Create the database connection with the MongoDB container.
Pass this connection to our API Controllers
Do the API Testing
Remove the MongoDB container after doing the testing.
M is a type passed to a TestMain function to run the actual tests 2.
Let’s implement the TestingMain
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var(testDbInstance*mongo.Database)funcTestMain(m*testing.M){log.Println("setup is running")testDB:=SetupTestDatabase()testDbInstance=testDB.DbInstancepopulateDB()exitVal:=m.Run()log.Println("teardown is running")_=testDB.container.Terminate(context.Background())os.Exit(exitVal)}
populateDB() function inserts some data to the database so we can do our testing.
Let’s check the SetupTestDatabase() which is basically creating the MongoDB container and creating the connection to that container.
typeTestDatabasestruct{DbInstance*mongo.DatabaseDbAddressstringcontainertestcontainers.Container}funcSetupTestDatabase()*TestDatabase{ctx,_:=context.WithTimeout(context.Background(),time.Second*60)container,dbInstance,dbAddr,err:=createMongoContainer(ctx)iferr!=nil{log.Fatal("failed to setup test",err)}return&TestDatabase{container:container,DbInstance:dbInstance,DbAddress:dbAddr,}}func(tdb*TestDatabase)TearDown(){_=tdb.container.Terminate(context.Background())}funccreateMongoContainer(ctxcontext.Context)(testcontainers.Container,*mongo.Database,string,error){varenv=map[string]string{"MONGO_INITDB_ROOT_USERNAME":"root","MONGO_INITDB_ROOT_PASSWORD":"pass","MONGO_INITDB_DATABASE":"testdb",}varport="27017/tcp"req:=testcontainers.GenericContainerRequest{ContainerRequest:testcontainers.ContainerRequest{Image:"mongo",ExposedPorts:[]string{port},Env:env,},Started:true,}container,err:=testcontainers.GenericContainer(ctx,req)iferr!=nil{returncontainer,nil,"",fmt.Errorf("failed to start container: %v",err)}p,err:=container.MappedPort(ctx,"27017")iferr!=nil{returncontainer,nil,"",fmt.Errorf("failed to get container external port: %v",err)}log.Println("mongo container ready and running at port: ",p.Port())uri:=fmt.Sprintf("mongodb://root:pass@localhost:%s",p.Port())db,err:=database.NewMongoDatabase(uri)iferr!=nil{returncontainer,db,uri,fmt.Errorf("failed to establish database connection: %v",err)}returncontainer,db,uri,nil}
Now that we have the mongo.Database, we can create the Repository and then we can create the PostsController.
func(uPostsController)CreateBook()echo.HandlerFunc{returnfunc(cecho.Context)error{req:=new(CreateBookRequest)iferr:=c.Bind(&req);err!=nil{returnc.JSON(http.StatusBadRequest,map[string]interface{}{"err":err.Error(),})}objId,err:=primitive.ObjectIDFromHex(req.AuthorId)iferr!=nil{returnc.JSON(http.StatusBadRequest,map[string]interface{}{"err":err.Error(),})}author,err:=u.repo.GetAuthorById(c.Request().Context(),objId.Hex())iferr!=nil{iferrors.Is(err,mongo.ErrNoDocuments){returnc.JSON(http.StatusNotFound,map[string]interface{}{"err":"author does not exist",})}}createdBook,err:=u.repo.CreateBook(c.Request().Context(),models.Book{Title:req.BookName,Author:*author,Likes:0,})iferr!=nil{returnc.JSON(http.StatusInternalServerError,map[string]interface{}{"err":err.Error(),})}returnc.JSON(http.StatusCreated,map[string]interface{}{"book":createdBook,})}}
it checks if the author exists
if author exists, then create the book in the database.
Here is an example request and response from the server.
packageintegrationtestimport("context""fmt""log""net/http""os""testing""github.com/labstack/echo/v4""github.com/ocakhasan/mongoapi/internal/controllers""github.com/ocakhasan/mongoapi/internal/repository""github.com/ocakhasan/mongoapi/pkg/router""github.com/steinfletcher/apitest""github.com/steinfletcher/apitest-jsonpath""go.mongodb.org/mongo-driver/mongo")var(testDbInstance*mongo.Database)funcTestMain(m*testing.M){log.Println("setup is running")testDB:=SetupTestDatabase()testDbInstance=testDB.DbInstancepopulateDB()exitVal:=m.Run()log.Println("teardown is running")_=testDB.container.Terminate(context.Background())os.Exit(exitVal)}funcInitializeTestRouter()*echo.Echo{postgreRepo:=repository.New(testDbInstance)userController:=controllers.New(postgreRepo)returnrouter.Initialize(userController)}funcTestCreatePostSuccess(t*testing.T){apitest.New().Handler(InitializeTestRouter()).Post("/api/book").Header("content-type","application/json").BodyFromFile("requests/create_book_success.json").Expect(t).Status(http.StatusCreated).BodyFromFile("responses/create_book_response.json").End()}
Let’s analyze the commands step by step.
apitest.New(): New creates a new api test. The name is optional and will appear in test reports
Handler(InitializeTestRouter()): initializes the endpoints and their corresponding handlers.
Post("/api/book").: sends a POST request to /api/book endpoint.
Header("content-type", "application/json").: sets the content-type header.
BodyFromFile("requests/create_book_success.json"): reads the body from given file and sets the request body.
Status(http.StatusCreated): expects the response status code to http.StatusCreated.
BodyFromFile("responses/create_book_response.json"): expects the body to be same as the given file content.
We send a request with given body and we expect the response to be in a certain format and certain data.
As we can see it is super easy to setup and test our endpoints.
Hope you enjoyed the blog. Once again, you may not grasp the whole concept by just looking at the code examples here, please check the golang-mongo-rest-api.