Back to all articles

Unit tests in Golang way

Hello everyone.

Today I want to speak about unit tests in Golang. I’m been working more than 7 years with Python and the way I test application was really different from Golang. For a first time, Golang might shock you, but you just need to understand ideas behind go. Once more, it is really different from a way you did a test with scripting languages (Python/PHP/JavaScript/Ruby and etc), I would like to provide my understanding and I'm really hoped to help anybody else to try to migrate on Golang.

Let's, start from a small example, if you worked with Django/Python you definitely saw code like this:

from django.test import Client
c = Client()
User.object.create({‘username’: ‘john’, ‘password’: ‘smith’})
response = c.post('/login/', {'username': 'john', 'password': 'smith'})
self.assert(response.status_code, 200)


Trying to do the same for Golang? Well, you are got in the wrong way. Probably you already search how to mock database or Redis in a Golang, how to mock functions and load fixtures, but you did not find anything and decide to write down your own solution?

Hey, stop for a minute!

Go trying to stay minimalistic and keep his simplicity everywhere. Go does not provide you a way to mock functions and it does not have all that whistles build in as other languages have. And it is maybe sound absurd but it is a strong side of a Golang development process.

You actually don't need it. Cause main idea of the unit tests is:

  • Test only code that you wrote;
  • Make it quick so you can run it after each saving.

Building tests on already existing codebase is a good way to check your code on architecture flaws. But, every time you thinks you are missing some toolbox from another language — double check yourself. Probably if you will think more you will find a solution that does not requires third-party libraries.

Even more, Golang force you to write tests first before you do any programming. But, let’s come back to the mocking problem. Here is a thing -- you actually don’t need to mock database, you don’t need to mock facebook API call or any other software you are using — let database developers test database.

All you need is to stay focus on your code:

And you might need to do refactoring of your existing method to be able to test it. Basically same happens to me when I used Python, while test process — refactor codebase to be more clear and easy to work with. Main different with Golang you have to think in a conception of steaming data. Database/file/memory read and write operations is just a bytes steam which you transfer from one place to other. So you don’t need to test a database driver work, all you need to do is to make sure logic you build is correct and meets the requirement.

I would like to take a small example of a login function (you can see the whole project by opening this link):

// New will create new App instance and setup storage connection
func New(host, user, password, dbname string) (a App, err error) {
   a = App{}

   if host == "" {
     log.Fatal("Empty host string, setup DB_HOST env")
     host = "localhost"
   }

   if user == "" {
     return a, fmt.Errorf("Empty user string, setup DB_USER env")
   }

   if dbname == "" {
     return a, fmt.Errorf("Empty dbname string, setup DB_DBNAME env")
   }

   connectionString :=
     fmt.Sprintf("host=%s user=%s password='%s' dbname=%s sslmode=dis

   a.DB, err = sql.Open("postgres", connectionString)
   if err != nil {
     return a, fmt.Errorf("Cannot open postgresql connection: %v", err)
   }

   a.Router = mux.NewRouter()
   a.initializeRoutes()
   return a, nil
}

func (a *App) login(w http.ResponseWriter, r *http.Request) {
   var u User
   decoder := json.NewDecoder(r.Body)
   err := decoder.Decode(&u)

   errors := checkLoginData(&u)

   if len(errors) > 0 {
     respondWithJSON(w, r, http.StatusBadRequest, errors)
     return    }

   if err := a.DB.QueryRow(“”); err != nil {
     errors["__error__"] = append(errors["__error__"], "email or password
   }

   if len(errors) > 0 {
     respondWithJSON(w, r, http.StatusBadRequest, errors)
     return    }

   respondWithJSON(w, r, 200, u)
}


I can see a few problems here:

  • The app created DB connection in New method;
  • SQL query in login function force us to use database.

Sure, I know that you can add abstraction likeUser.get or something similar. The problem in a fact how you treat your data.

To test this application we would need:

  • Setup the test database, get it up and ready to accept connections;
  • Upload fixtures and test data;
  • Run test server;
  • Initialize full application;
  • Test your code;
  • Read and write data to database;
  • Close application;
  • Clear database.

All of this operations is time-consuming. And you would need to fake 80% of this to create a test environment for yourself. Considering how much of developers time wasted maintaining and running unit tests, it may become an undesirable task and manual testing seems like a less expensive solution.

Golang provides us simple, until powerful, solution for all these problems. From all list above we only need to left one item: test your code.

So how do we do this? Stop thinking about what you should save to the database. All we did here is just transform bytes from one form to other. A unique interface with concrete functions and without dependency on software will save us.

// Storage provider can handle read/write for our application
type Storage interface {
   GetUserByEmail(*User) error
   CreateUser(*User) error
}

In addition we will need structures for live application and for test one:

// PGStorage provider can handle read/write from database
type PGStorage struct {
   con *sql.DB
}

type FakeStorage struct {
   data User
   resp string
   code int
}


With a tests we make assumption that our database is working fine (you may still tell that your SQL queries might be wrong and we need to check them, i'll explain that later), so all we need to do is to fake SQL requests/responses. Even more, we can just use a table tests with assumption what data correct and vice versa.

Again, that might not have a sense for a first sight but if you look closer, you might see that you did the same before on Python/PHP/Ruby/JavaScript with:

User.create({‘username’: ‘john’, ‘password’: ‘smith’})

I.e. create data functions is "equal" to Golang table data (full example):

FakeStorage{
   data: User{
     Email: "new@user.com",
     Password: "123123"
   },
   resp: `{"id":1}`,
   code: 201
},


Python code might give you more mental confidence but the price of that confidence is too high. More important — too much time spend on unit tests may leave you without integration tests which will guarantee bugs in production.

So don't try to reinvent the wheel, don't test piece of the application that already been tested by other developers — try to test your code, don't spend too much time on getting 100% coverage. Leave time and energy for integration tests you will need them anyway.

For the folks who think integration tests is hard, it is not. To test real SQL queries I just did (I assume you have HTTPie if not you can use curl):

I'll get all errors during real SQL queries using this 4 lines script.

It took 3 seconds to execute, and it covered places avoided with unit tests.

PREVIOUS ARTICLENEXT ARTICLE