Click here to Skip to main content
14,576,820 members

Simple, Stupid, Effective Tests in GO

Rate this:
0.00 (No votes)
Please Sign up or sign in to vote.
0.00 (No votes)
1 Jun 2020CPOL
This post will discuss ways to overcome simple, yet annoying problems that come up in the process of writing code by writing tests.

What

This is not about fancy TDD practices, what test frameworks you must use, or why [insert your favorite oldish (as in 2010s) jargon] cannot answer the exceeding amount of sophistication in our new software.

Image 1

This is about how we can overcome simple, yet annoying problems that come up in the process of writing code by writing tests.

I’m going to talk about go here. Probably none of these can be applied to any other language/environment directly because of the differences in the language features, best practices, and/or conventions. However, I believe the analogy is a reusable one and therefore more general than just golang since I have caught myself thinking about the same practices in other situations.

Throughout this post, I’ll sometimes refer to a hypothetical caching interface defined as:

type CacheService interface {
    Get(string) ([]byte, error)
    Set(string, []byte)
}

in the package github.com/someone/cache. (No, it does not actually exist [as of this writing].)

1. Test struct “constructors”

The first thing that we write in a new .go file is often a struct. Suppose we’re going to implement the above interface on top of a redis backend. Let’s call the implementing struct RedisCacheService. Let’s start off by writing a test:

file: cache/redis/service_test.go

package redis

import (
    "testing"
)

func TestRedisCacheService_Initialization(t *testing.T) {
    host := "localhost"
    port := "6379"
    _ = NewRedisCacheService(host, port)
}

file: cache/redis/service.go

package redis

type RedisCacheService struct {
}

func NewRedisCacheService(host, port string) *RedisCacheService {
    ret := &RedisCacheService{
        // host, port and connection init, connection pool, etc.
    }
    return ret
}

This is a very simple test. It may even look too simple since there is not even an assertion in the body. That’s because the compiler can do all the necessary work here by checking the function signature.

Writing this seemingly trivial test against the NewRedisCacheService(string, string) function not only allows you to advance src_test.go and src.go together (in that order) from early on in a full TDD manner, but also allows you to think about the usage of the “ctorbefore actually writing it. Its easy to stay with an obviously problematic “ctor” interface as a result of justifying it in hindsight after you’ve written it down.

Also, regarding the general testing philosophy, you may not be the actual user of NewRedisCacheService and therefore may have no way of finding out about breaking changes to this interface when you recompile.
This could also be library code and it could well be the case that you’re testing each functionality of the CacheService by manually initializing the RedisCacheService struct (as opposed to calling NewRedisCacheService) for example, to mock some resource internal to RedisCacheService to which you don’t have access through the “ctor”.

The consistency of the ctor is easy to overlook when you’re writing tests, since it’s not an actual “functionality” of a defined interface, and it is obviously very destructive if an unwanted change is introduced.

2. Test Interface Implementations

Your implementation of the CacheService needs to be correct in terms of language semantics: The struct needs to define the methods specified by the interface.

This is also hard to keep track of as there might not always be explicit calls to every interface functions or pieces of code to “cast” up from structs to interfaces for the implementations of the functions to be checked. Also in the early developments of a new module, you would risk pushing semantically incorrect code to the upstream since there could be no usage of the new code yet.

Let’s keep things simple:
Add to file: cache/redis/service.go

import (
    cache "github.com/someone/cache"
)

func TestRedisCacheService_InterfaceImplementation(t *testing.T) {
    var _ cache.CacheService = &RedisCacheService{}
}

Again, there’s no need to assert anything. The compiler is doing all the work by checking whether or not RedisCacheService implements cache.CacheService. Note how we kept it as minimal as possible by not calling the ctor and creating the object directly.

3. Test Generated Assets: Mocks

When you specify the interfaces through which your program modules are supposed to talk to each other in the top level and only communicate through those interfaces, interesting stuff tends to happen. Here’s an example from digest:

//go:generate mockgen --source=digest.go --destination=mock/mock.go --package=mock

package digest

type DocsService interface {
    TakeAndPersistSnapshopt(string) error
}

type SMTPService interface {
    SendMail(to string, msg []byte) error
    SendMailHtml(to, subject string, msg []byte) error
}

type DiffService interface {
    DiffDirsHtml(string, string) string
}

A particularly pleasant thing is that you can generate all (well, except third parties) the mocks that you would need (you only communicate through these, remember?) from this file.

The weird thing about auto generated stuff however is that you don’t know when they get out dated, and I don’t think you should. Let’s write a test to handle this.

I tend to write this kind of test in a file right next to the services file. In digest, the file is named digest.go in the root, and therefore:

file: digest_test.go:

package digest

import (
    "fmt"
    "io/ioutil"
    "os"
    "os/exec"
    "strings"
    "testing"

    "github.com/stretchr/testify/assert"
)

func TestMockGeneration(t *testing.T) {
    tmpFile, err := ioutil.TempFile("", "digest_go_mock")
    assert.Nil(t, err)
    defer func() {
        os.Remove(tmpFile.Name())
    }()

    fullMockgenString := 
        fmt.Sprintf(`mockgen --source digest.go --destination %s --package=mock`,
        tmpFile.Name())
    mockgenCmdArgs := strings.Split(fullMockgenString, " ")
    mockgenCmd := exec.Command(mockgenCmdArgs[0], mockgenCmdArgs[1:]...)

    err = mockgenCmd.Start()
    assert.Nil(t, err)

    err = mockgenCmd.Wait()
    assert.Nil(t, err)

    originalMockContents, err := ioutil.ReadFile("./mock/mock.go")
    assert.Nil(t, err)

    newMockContents, err := ioutil.ReadFile(tmpFile.Name())
    assert.Nil(t, err)

    assert.Equal(t, newMockContents, originalMockContents)
}

Now everytime you run your tests (speaking of which, I really suggest making use of goconvey which runs them automatically for you), you’re going to be notified of a possible change in your interfaces’ API that you forgot to reflect in your mocks by regenerating them.

4. Test Generated Assets: Other Things

I really liked go-bindata and still find it quite useful even though it has been archived. However, a problem similar to that of mocks is waiting to hunt you down if you use generated/bundled static assets through language APIs. Forgetting to regenerate your bindings can cause problems that are hard to track down and waste some time that we might as well spend watching cat videos on youtube.

This one came in really handy in shopapi where I was using graphql. graph-gophers/graphql-go nicely verifies your code against your schema but as I was using go-bindata to feed my schema to graphql-go, I would repeatedly forget to regenerate the static assets, which basically made the library unaware of my new changes to the graphql schema. The solution, as you might guess, is pretty simple:

package graphql

import (
    "io/ioutil"
    "testing"

    "github.com/stretchr/testify/assert"
)

func TestReadSchema(t *testing.T) {
    schemaContentFromBindata := Schema()

    actual, err := ioutil.ReadFile("./schema.graphql")

    if err != nil {
        assert.FailNow(t, "error reading the schema file")
    }

    assert.Equal(t, string(actual), schemaContentFromBindata)
}

Now let’s watch some cat videos.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

I make things faster.

https://blog.farnasirim.ir

Comments and Discussions

 
GeneralMessage Closed Pin
1-Jun-20 3:08
MemberoIlayda Oei1-Jun-20 3:08 
QuestionFormat Pin
Nelek30-May-20 1:18
protectorNelek30-May-20 1:18 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Technical Blog
Posted 1 Jun 2020

Stats

1.5K views