gspec

package module
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Aug 2, 2023 License: MIT Imports: 5 Imported by: 0

README

gspec

Go project version Go Report Card codecov gosec GitHub

gspec is a testing framework for Go, inspired by Ruby's rspec.

Installation

go get github.com/broothie/gspec

Usage

Basics

gspec hooks into Go's built-in testing framework.

  1. In regular Go test function, gspec.Describe or gspec.Run are used to open a gspec context.
  2. Then, c.It is used to define an actual test case.
  3. Within a test case, c.Assert() returns an *assert.Assertions, which can be used to make assertions about the code under test.
package examples

import (
   "testing"

   "github.com/broothie/gspec"
)

func Test(t *testing.T) {
   gspec.Describe(t, "addition", func(c *gspec.Context) {
      c.It("returns the sum of its operands", func(c *gspec.Case) {
         c.Assert().Equal(3, 1+2)
      })
   })
}

If you need to access the underlying *testing.T, you can do so from within a hook or test case via c.T().

package examples

import (
   "testing"

   "github.com/broothie/gspec"
)

func somethingThatNeedsTestingT(t *testing.T) {}

func Test_t(t *testing.T) {
   gspec.Describe(t, ".T", func(c *gspec.Context) {
      c.It("returns a *testing.T", func(c *gspec.Case) {
         somethingThatNeedsTestingT(c.T())
      })
   })
}
Groups

Test cases can be grouped together via c.Describe and c.Context. Groups can be nested arbitrarily. Groups inherit Lets and hooks from their parents.

package examples

import (
   "testing"

   "github.com/broothie/gspec"
)

func Test_groups(t *testing.T) {
   gspec.Run(t, func(c *gspec.Context) {
      c.Describe("some subject", func(c *gspec.Context) {
         c.Context("when in some context", func(c *gspec.Context) {
            c.It("does something", func(c *gspec.Case) {
               // Test code, assertions, etc.
            })
         })
      })
   })
}
Let

gspec.Let allows for the definition of type-safe, per-case values. Let values are only evaluated if they are used in a test case, and are cached for the duration of the test case.

Let values can be overwritten in nested groups, but their return type must remain the same. When overwriting a Let in this way, the returned function needn't be captured. The value will still be registered for the context, even though the function was captured in an outer group.

package examples

import (
   "strings"
   "testing"

   "github.com/broothie/gspec"
)

func capitalize(input string) string {
   return strings.ToUpper(input)
}

func Test_capitalize(t *testing.T) {
   gspec.Run(t, func(c *gspec.Context) {
      input := gspec.Let(c, "input", func(c *gspec.Case) string { return "Hello" })

      c.It("should capitalize the input", func(c *gspec.Case) {
         c.Assert().Equal("HELLO", capitalize(input(c)))
      })

      c.Context("with spaces", func(c *gspec.Context) {
         gspec.Let(c, "input", func(c *gspec.Case) string { return "Hello, world" })

         c.It("should capitalize the input", func(c *gspec.Case) {
            c.Assert().Equal("HELLO, WORLD", capitalize(input(c)))
         })
      })
   })
}
Hooks

c.BeforeEach and c.AfterEach can be used to register hooks that run around each test case.

Hooks are inherited by nested groups.

package examples

import (
   "fmt"
   "net/http"
   "net/http/httptest"
   "testing"

   "github.com/broothie/gspec"
)

func Test_hooks(t *testing.T) {
   gspec.Run(t, func(c *gspec.Context) {
      mux := gspec.Let(c, "mux", func(c *gspec.Case) *http.ServeMux { return http.NewServeMux() })
      server := gspec.Let(c, "server", func(c *gspec.Case) *httptest.Server { return httptest.NewServer(mux(c)) })

      c.BeforeEach(func(c *gspec.Case) {
         mux(c).HandleFunc("/api/teapot", func(w http.ResponseWriter, r *http.Request) {
            w.WriteHeader(http.StatusTeapot)
         })
      })

      c.AfterEach(func(c *gspec.Case) {
         server(c).Close()
      })

      c.It("serves requests", func(c *gspec.Case) {
         response, err := http.Get(fmt.Sprintf("%s/api/teapot", server(c).URL))
         c.Assert().NoError(err)
         c.Assert().Equal(http.StatusTeapot, response.StatusCode)
      })
   })
}

RSpec Feature Comparison

Feature gspec
Example Groups
Let
Hooks
Mocks Use an existing mock library, such as https://github.com/uber-go/mock.
Fluent-syntax expectations *gspec.Case exposes assertions from assert via c.Assert().

Why?

Go's built-in testing utilities are pretty good on their own. Paired with a library like assert and Go testing is pretty dang good.

I think the power of this package comes from Let, and how it works with groups. Go's t.Run and its use of closures makes it difficult/confusing to define reusable values in an outer scope which can be overwritten in an inner scope. Plus, having multiple tests that close over the same value runs the risk of modification of that shared value.

Let values are per-case, lazy-evaluated, overwrite-able, and cached for the duration of the test case. Since they're overwrite-able, a Let can be redefined for a subgroup, even if they're not specifically referenced from within that group's test cases.

package examples

import (
  "testing"

  "github.com/broothie/gspec"
)

type Parser struct {
  index  int
  tokens []string
}

func (p *Parser) IsExhausted() bool {
  return p.index >= len(p.tokens)
}

func Test_advanced_let(t *testing.T) {
  gspec.Describe(t, "Parser", func(c *gspec.Context) {
    tokens := gspec.Let(c, "tokens", func(c *gspec.Case) []string {
      return []string{"arg1", "arg2", "-f", "filename"}
    })

    parser := gspec.Let(c, "parser", func(c *gspec.Case) *Parser { return &Parser{tokens: tokens(c)} })

    c.Describe(".IsExhausted", func(c *gspec.Context) {
      c.Context("when tokens remain", func(c *gspec.Context) {
        c.It("is false", func(c *gspec.Case) {
          c.Assert().False(parser(c).IsExhausted())
        })
      })

      c.Context("when no tokens remain", func(c *gspec.Context) {
        c.BeforeEach(func(c *gspec.Case) {
          parser(c).index = 4
        })

        c.It("is true", func(c *gspec.Case) {
          c.Assert().True(parser(c).IsExhausted())
        })
      })

      c.Context("when tokens is empty", func(c *gspec.Context) {
        gspec.Let(c, "tokens", func(c *gspec.Case) []string { return nil })

        c.It("is true", func(c *gspec.Case) {
          c.Assert().True(parser(c).IsExhausted())
        })
      })
    })
  })
}

Documentation

Overview

Package gspec provides a collection of test helpers that form a test framework.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Describe

func Describe(t testingT, subject string, f ContextFunc)

Describe opens a root test group labelled by the provided subject.

func Run

func Run(t testingT, f ContextFunc)

Run opens a root test group without a label.

Types

type Case

type Case struct {
	// contains filtered or unexported fields
}

Case provides a handle for test cases to make assertions via *Case.Assert. It also provides *Case.Require for assertions that immediately fail the test case.

func (*Case) Assert

func (c *Case) Assert() *assert.Assertions

Assert provides a reference to a test case's *assert.Assertions.

func (*Case) Require

func (c *Case) Require() *require.Assertions

Require provides a reference to a test case's *require.Assertions.

func (*Case) T added in v0.1.1

func (c *Case) T() *testing.T

T provides the test case's underlying *testing.T.

type CaseFunc

type CaseFunc func(c *Case)

CaseFunc is the signature of functions passed in (typically anonymously) to *Context.It, *Context.BeforeEach, and *Context.AfterEach.

type Context

type Context struct {
	// contains filtered or unexported fields
}

Context provides a handle for test groups to define test cases, nested groups, lets, and hooks.

func (*Context) AfterEach

func (c *Context) AfterEach(f CaseFunc)

AfterEach registers a hook to run after each test case.

func (*Context) BeforeEach

func (c *Context) BeforeEach(f CaseFunc)

BeforeEach registers a hook to run before each test case.

func (*Context) Context

func (c *Context) Context(context string, f ContextFunc)

Context defines a nested group labelled with the provided context. Context labels typically begin with "when", "with", or "without".

func (*Context) Describe

func (c *Context) Describe(subject string, f ContextFunc)

Describe defines a nested group labelled with the provided subject.

func (*Context) It

func (c *Context) It(behavior string, f CaseFunc)

It defines a test case labelled with the provided behavior.

type ContextFunc

type ContextFunc func(c *Context)

ContextFunc is the signature of functions passed in (typically anonymously) to gspec.Run, gspec.Describe, *Context.Describe, and *Context.Context.

type LetFunc

type LetFunc[T any] func(c *Case) T

func Let

func Let[T any](c *Context, name string, f LetFunc[T]) LetFunc[T]

Let defines a value to be retrieved from within a later-defined test case or hook. It returns a function which can be called within a test case or hook to retrieve the value.

Let values are only evaluated if they're called within a test case or hook, and the value is cached for the duration of the test case.

Let values can be overwritten in nested groups, but their return type must remain the same. When overwriting a Let in this way, the returned function needn't be captured. The value will still be registered for the context, even though the function was captured in an outer group.

Directories

Path Synopsis
Package mocks is a generated GoMock package.
Package mocks is a generated GoMock package.

Jump to

Keyboard shortcuts

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