03-testing/

directory
v0.0.0-...-733830a Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 14, 2020 License: MIT

README

Testing in Go

test types

  • Unit - most granular type of test and aimed at testing individual pieces and the smallest units possible. It tests at line level, individual function level
  • Integration - tests code in its immediate surroundings. As soon as you are not using a mocked response it is an integration test.
  • Functional - full running tests, or end-to-end tests.

We can visualise that in the context of a solution as follows, note that parts making up the integration test layer will themselves be subject to unit tests as the various sets in the functional test will have both unit and integration level test layers.

With the types established we should now present the typical perfect scenario prescribed with the testing pyramid that sees 70% of total coverage at unit level, 20% at integration and 10% at end to end functional coverage. The model is great but we live in a imperfect world and sometimes we are forced to make tactical solutions, to ship software without adequate time for full testing coverage and other times we made be bolstering and retrofitting tests to existing software that is being reinvested in.

Unit testing

As we have said above the idea of the unit test is to test individual functions, statements and lines of code. A general rle of thumb is that we should have tests for every return of a function, that will typically catch early return for failure or bad scenarios as well as positive test cases and result returns.

In the example here, we're going to test the user-dao.go file from our domain layer. Specifically we're going to test the GetUsers function which has the following code

// GetUser return the user or error
func GetUser(userID int64) (*User, *utils.ApplicationError) {
	// implementation
	if user := users[userID]; user != nil {
		return user, nil
	}

	return nil, &utils.ApplicationError{
		Message:    fmt.Sprintf("user %v was not found", userID),
		StatusCode: http.StatusNotFound,
		Code:       "Not found",
	}
}

We can observe that:

  • We have two returns to test
  • we have an error scenario where a user is not found.
  • We have a return where a user was matched.

We can tell that the type of things we will want to check in the error situation are:

  • an error was raised
  • the error Code was as expected
  • the error Message was correct
  • the http.StatusCode is correct.

Just as for a valid situation we should ensure:

  • the expected and actual ID do match
  • Other values are as expected.

This fits the unit model in that we have a mocked database and this is a situation that can be mocked quite trivially and is isolated processing therefore it makes for a good unit test basis. So let's see how that looks. Note, we will use an additional import here as Go has no built-in assert library as found in some other languages but we have a 3rd party tool that offers the same job. If you object to a 3rd party addition for test code simplification you can achieve the same results with if statements and supply the testing object, the expected condition and the actual condition.

Running the tests

Now we can run the tests from the directory that contains them, which should be the same as the code itself, with go test or you can call the enclosing directory from the $GOPATH start point as the argument to go test.

eg: assume our code is in the $GOPATH/src/github.com/username/mvc/domain directory. If we pass this as the argument the go tool will find the correct file, one which mimics the name of the go file under test and with a _test.go suffix.

Benchmarks

Here we're using a simple Bubble sort largely because its a notable inefficient way to sort. However, if we dig deeper we can see that actually a bubble sort is ne of the best operations and mot efficient if you have a very small number of elements, but the resource intensity of it is exponential, so as the elements number increases the performance drops off a cliff. We Can use the testify library again here with the testing.B for benchmarking.

As a comparison we're using a simple handcoded bubble sort up against the Go standard library sort.Ints and we can see that for cases of 10, 1000 & 100,000 numbers the go standard library is actually slower than a simple handcoded bubble sort, but as the number ramps up the standard library sort requires far fewer iterations to get a settled benchmark and it is significantly quicker than the one we have coded.

package utils

// BubbleSort sorting algo
func BubbleSort(elements []int) []int {
	running := true

	for running {
		running = false

		for i := 0; i < len(elements)-1; i++ {
			if elements[i] > elements[i+1] {
				elements[i], elements[i+1] = elements[i+1], elements[i]
				running = true
			}
		}
	}
	return elements
}

and we will test the code with the following routines.

package utils

import (
	"sort"
	"testing"

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

func TestBubbleSortWorstCaseScenario(t *testing.T) {
	// initialisation section
	// creates a slice where every element will require processing/swapping
	eles := []int{9, 8, 7, 6, 5, 4, 3, 2, 1}

	// execution section
	eles = BubbleSort(eles)

	// validation section
	// tests return is not nil
	assert.NotNil(t, eles)

	// check length of return is equal to input
	assert.EqualValues(t, 9, len(eles))

	// checks individual elements against
	// expected sort ascending
	assert.EqualValues(t, 1, eles[0])
	assert.EqualValues(t, 2, eles[1])
	assert.EqualValues(t, 3, eles[2])
	assert.EqualValues(t, 4, eles[3])
	assert.EqualValues(t, 5, eles[4])
	assert.EqualValues(t, 6, eles[5])
	assert.EqualValues(t, 7, eles[6])
	assert.EqualValues(t, 8, eles[7])
	assert.EqualValues(t, 9, eles[8])

}

func TestBubbleSortBestCaseScenario(t *testing.T) {
	eles := []int{1, 2, 3, 4, 5}
	eles = BubbleSort(eles)

	assert.NotNil(t, eles)
	assert.EqualValues(t, 5, len(eles))

	// checks individual elements against
	assert.EqualValues(t, 1, eles[0])
	assert.EqualValues(t, 2, eles[1])
	assert.EqualValues(t, 3, eles[2])
	assert.EqualValues(t, 4, eles[3])
	assert.EqualValues(t, 5, eles[4])
}

func TestBubbleSortNilCaseScenario(t *testing.T) {
	eles := BubbleSort(nil)
	assert.Nil(t, eles)
}

func getElements(n int) []int {
	res := make([]int, n)
	i := 0
	for j := n - 1; j >= 0; j-- {
		res[i] = j
		i++
	}
	return res
}

func BenchmarkBubbleSort1000(b *testing.B) {
	eles := getElements(1000)
	for i := 0; i < b.N; i++ {
		BubbleSort(eles)

	}
}

func BenchmarkBubbleSort100000(b *testing.B) {
	eles := getElements(100000)
	for i := 0; i < b.N; i++ {
		BubbleSort(eles)

	}
}

func BenchmarkSort1000(b *testing.B) { // we're still slower on 1000 elements
	eles := getElements(1000)
	for i := 0; i < b.N; i++ {
		sort.Ints(eles)

	}
}

When we ramp this up to 100,000

so the optimal use of what we can see here is that somewhere between 1000 and say 50000 is the magic number where the Go native sort becomes much more efficient, now architectures are all very different but for me, it's nowhere near 50000. I'm actually happy with a flip of around 2000 so we can use a routine something like below.

Directories

Path Synopsis
src
app

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL