Skip to content

JosiahWitt/ensure

Repository files navigation

ensure

A balanced test framework for Go.

Documentation CI Go Report Card codecov

Go Version Support

Only the last two minor versions of Go are officially supported.

Install

Library

$ go get github.com/JosiahWitt/ensure

CLI

# Requires Go 1.21+
$ go install github.com/JosiahWitt/ensure/cmd/ensure@latest

About

Ensure supports Go 1.13+ error comparisons (using errors.Is), and provides easy to read diffs (using deep.Equal). Ensure also supports mocks using GoMock.

Ensure was partially inspired by the is testing mini-framework.

Overview

Creating a test instance starts by calling:

ensure := ensure.New(t)

Then, ensure can be used as a function to asset a value is correct, using the pattern ensure(<actual>).<Method>(<expected>). Methods can also be called on ensure, using the pattern ensure.<Method>().

Configuring the CLI

The ensure CLI is configured using a .ensure.yml file which is located in the root of your Go Module (next to the go.mod file). Source code for the ensure CLI is located in the cmd/ensure directory.

Here is an example .ensure.yml file:

mocks:
  # Used as the directory path relative to the root of the module
  # for any interfaces that are not within internal directories.
  # Optional, defaults to "internal/mocks".
  primaryDestination: internal/mocks

  # Used as the directory path relative to internal directories within the project.
  # Optional, defaults to "mocks".
  internalDestination: mocks

  # Tidy mocks after generation completes.
  # Automatically runs 'ensure mocks tidy' after 'ensure mocks generate' completes.
  # Tidy removes any files that would not be generated by the provided packages list.
  # Optional, defaults to true.
  tidyAfterGenerate: true

  # Packages with interfaces for which to generate mocks
  packages:
    - path: github.com/my/app/some/pkg
      interfaces: [Iface1, Iface2]

Examples

Basic Testing

func TestBasicExample(t *testing.T) {
  ensure := ensure.New(t)
  ...

  // Methods can be called on ensure, for example, Run:
  ensure.Run("my subtest", func(ensure ensuring.E) {
    ...

    // To ensure a value is correct, use ensure as a function:
    ensure("abc").Equals("abc")
    ensure(produceError()).IsError(expectedError)
    ensure(doNotProduceError()).IsNotError()
    ensure(true).IsTrue()
    ensure(false).IsFalse()
    ensure("").IsEmpty()

    // Failing a test directly:
    ensure.Failf("Something went wrong, and we stop the test immediately")
  })
}

Table Driven Testing

func TestTableDrivenExample(t *testing.T) {
  ensure := ensure.New(t)

  table := []struct {
    Name    string
    Input   string
    IsEmpty bool
  }{
    {
      Name:    "with non empty input",
      Input:   "my string",
      IsEmpty: false,
    },
    {
      Name:    "with empty input",
      Input:   "",
      IsEmpty: true,
    },
  }

  ensure.RunTableByIndex(table, func(ensure Ensure, i int) {
    entry := table[i]

    isEmpty := strs.IsEmpty(entry.Input)
    ensure(isEmpty).Equals(entry.IsEmpty)
  })
}

Table Driven Testing with Mocks

Mocks can be generated by running ensure mocks generate, which wraps GoMock. To install the ensure CLI, see the Install section.

// db/db.go
type DB interface {
  Put(id string, data interface{}) error
  ...
}

// user/user.go
type UserStorage struct {
  DB db.DB
  ...
}

type User struct {
  ID    string
  Name  string
  ...
}

func (s *UserStorage) Save(ctx context.Context, u *User) error { ... }

// user/user_test.go
func TestTableDrivenMocksExample(t *testing.T) {
  ensure := ensure.New(t)

  type Mocks struct {
    mocksets.DefaultMocks
    DB *mock_db.MockDB // Mock of the db.DB interface generated by `ensure mocks generate`
  }

  table := []struct {
    Name          string
    Input         *user.User
    ExpectedError error

    Mocks      *Mocks            // Mocks to automatically initialize
    SetupMocks func(*Mocks)      // Optional function to allow for mock setup
    Subject    *user.UserStorage // Optional subject containing interfaces with which to assign the mocks
  }{
    {
      Name:    "with valid user",
      Input:   &user.User{
        ID:   "my-id",
        Name: "Mary",
      },
      SetupMocks: func(m *Mocks) {
        m.DB.EXPECT().Put("my-id", &user.User{
          ID:   "my-id",
          Name: "Mary",
        })
      },
    },
    {
      Name:    "with missing ID",
      Input:   &user.User{
        ID:   "",
        Name: "Mary",
      },
      SetupMocks: func(m *Mocks) {
        m.DB.EXPECT().Put("", &user.User{
          ID:   "",
          Name: "Mary",
        }).Return(errors.New("missing ID"))
      },
      ExpectedError: user.ErrSavingUser,
    },
  }

  ensure.RunTableByIndex(table, func(ensure Ensure, i int) {
    entry := table[i]

    err := entry.Subject.Save(entry.Mocks.Context, entry.Input)
    ensure(err).IsError(entry.ExpectedError)
  })
}

// mocksets/mocksets.go
type DefaultMocks struct {
  // Tag suppresses warning when it isn't used in the Subject
  Context *mockctx.MockContext `ensure:"ignoreunused"`
}

// mockctx/mockctx.go
type MockContext struct { context.Context }

// NEW method allows creating a MockContext, and is automatically called by ensure.
func (*MockContext) NEW() *MockContext {
  return &MockContext{Context: context.Background()}
}