![Go logo](img/go-logo.svg) <!-- .element style="border: none; box-shadow: none; height: 100px" --> ## Test writing workshop --- # Testing -- ## Why? -- ## How? -- ### TDD -- ### Test first -- ### Prototype first -- ## What? -- ### Unit level -- ### Integration level --- ![Go logo](img/go-logo.svg) <!-- .element style="border: none; box-shadow: none; height: 100px" --> ## Test framework -- ### Test types - Benchmark - Example - Test -- ### Anatomy of a test files ending with `_test.go` functions starting with `Test[A-Z]` `func(t *testing.T)` signature -- ```go import "testing" func TestSomething(t *testing.T) { // test something // this will succeed } ``` -- ### Subtests ```go import "testing" func TestSomething(t *testing.T) { t.Run("name", func(t *testing.T) { }) t.Run("name2", func(t *testing.T) { }) } ``` -- ### Test output/result - `t.Log(f)` - `t.Error(f)` - `t.Fatal(f)` - `t.Skip(f)` -- ### Parallel test running ```go import "testing" func TestSomething(t *testing.T) { t.Parallel() } ``` -- ### Running tests ```bash # Run tests go test # Run tests for all packages go test ./... # Run tests in verbose mode to see every output go test -v # Run a specific test go test -run ^TestIntegration$ ``` --- ## Go test practices -- ### Manual assertions ```go if want, have := SOME_VALUE, result; want != have { t.Errorf("value does not match the expected one\n"+ "actual: %s\n"+ "expected: %s", have, want, ) } ``` -- ### `_test` package - useful to avoid cyclic dependencies - blackbox testing - lets you use qualified names in examples -- ```go // mypkg_test is allowed to be in the same directory as mypkg package mypkg_test import ( "testing" "mypkg" ) func TestSomething(t *testing.T) { // this is fine mypkg.New() // this will fail mypkg.new() } ``` -- ### Table-driven tests ```go func TestSomething(t *testing.T) { tests := struct{ name string input int output int }{ { name: "example name", input: 1, output: 2, }, } // ... } ``` -- ### Table-driven tests ```go func TestSomething(t *testing.T) { // ... for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { t.Parallel() // do the test }) } } ``` -- ### Integration tests ```go // +build integration package mypkg // integration tests ``` ```bash go test -tags integration ``` -- ### Integration tests ```go func TestIntegration(t *testing.T) { if m := flag.Lookup("test.run").Value.String(); m == "" || !regexp.MustCompile(m).MatchString(t.Name() ) { t.Skip("skipping as execution was not requested"+ "explicitly using go test -run") } t.Parallel() t.Run("SomeIntegrationTest", testSomeIntegrationTest) } ``` --- ## Testify -- `github.com/stretchr/testify` -- ### `assert` `assert.Equal(t, expected, actual)` uses `t.Error` under the hood -- ### `require` `require.NoError(t, err)` uses `t.Fatal` under the hood -- ### `mock` ```go import ( "github.com/stretchr/testify/mock" ) type MockSomething struct { mock.Mock } // implement Something interface func (m *MockSomething) DoSomething(number int) (bool, error) { args := m.Called(number) return args.Bool(0), args.Error(1) } ``` -- ### `mock` ```go func TestSomething(t *testing.T) { something := new(MockSomething) something.On("DoSomething", 1).Return(true, nil) // use something something.AssertExpectations(t) } ``` -- ### Mockery `github.com/vektra/mockery` ```bash mockery -name Something -inpkg -testonly ``` --- ## Example 1: Calculator -- ### Outline `Calculate` function accepts a number `x` and calculates a magic value. -- ### Task 1 `Calculate` returns `x+2` ---- *Hint: write a single test method with manual assertion* -- ### Task 2 `Calculate` returns: - `x+1` if `x` is odd - `x+2` otherwise ---- *Hint: write subtests with testify assertions* -- ### Task 3 `Calculate` returns: - `0` if `x` is zero - `x+1` if `x` is odd - `x+2` otherwise ---- *Hint: write table-driven tests* --- ## Example 2: Car Rental -- ### Outline Implement a Car Rental service that finds a car matching certain criteria: - color - brand - model (car should match at least brand and model, color is optional) -- ### Outline Return information to the caller: - Car information -- <img src="img/car-rental-service-1.svg" alt="Car rental service" class="image stretch"> -- ### Task 1 Implement the car rental service Defer making decisions on finding cars ---- *Hint: generate mocks* -- ### Task 2 Implement a Car repository Write tests for business logic (find a matching car) ---- *Hint: use table-driven tests* -- ### Task 3 Rewrite tests for the car rental service using real implementations. -- ### Task 4 Add generated ID to the returned Rent. ---- *Hint: mocks first then real implementation* -- <img src="img/car-rental-service-2.svg" alt="Car rental service" class="image stretch"> -- ### Task 4 Write integration tests for remote ID generator ---- *Hint: use any of the integration test strategies* --- # The End