Home

Awesome

Note:

This has not been maintained and/or updated since 2011. Perhaps consider corbym/gocrest, instead.

Introduction

Hamcrest is a fluent framework for declarative Matcher objects that, when applied to input values, produce self-describing results.

Installation:

To install, run make install from the same directory as this README.md file.

Packages

hamcrest.go comes in several packages that you assemble to fit your needs:

You may also choose to write your own Matchers (see Custom matchers, below).

How to use hamcrest for testing:

To use Hamcrest matchers, create an Asserter and use it to Check or Assert that values meet the criteria of those matchers:

func TestPoint(t *testing.T) {
	p := Point(3, 4)
	we := asserter.Using(t)
	we.AssertThat(p.X, EqualTo(3).Comment("x coord"))
	we.AssertThat(p.Y, EqualTo(4).Comment("y coord"))
	we.CheckThat(p, ToString(EqualTo("[3, 4]")))
	we.CheckThat(p.Magnitude(), EqualTo(5).Comment("magnitude"))
}

(Assert methods fail immediately, as testing.T.FailNow() does, while Check methods defer failure, as testing.T.Fail() does.)

The AssertThat and CheckThat functions are designed to create conditional checks that read fluently as self-commenting code, and are self-describing when failures occur. For example, the above test might fail with this message:

FAILURE on input &Point{X:3, Y:4}
	Did not match ToString(EqualTo([3, 4]))
	Because: String() was "[4, 3]"
		Did not match EqualTo[[3, 4]]
		Because: "[4, 3]" was not equal to "[3, 4]"

Or: FAILURE on input 5 Did not match EqualTo(5) Because: uint 5 could not be compared to int 5 Comment: magnitude

Note that the majority of the text descriptions are generated automatically by the matchers. For typical uses of Hamcrest matchers, the code is largely self-documenting, and the error messages are detailed.

Effort invested in good self-describing matchers can be leveraged across many tests.

Examples of using Hamcrest at runtime:

Just as in the testing example, create an Asserter, but use a factory method such as UsingStderr(), which returns an Asserter that logs problems to stderr and calls panic on FailNow:

import (
	"github.com/rdrdr/hamcrest/asserter"
)

var we = asserter.UsingStderr()

Use that asserter during init() to make sure globals are properly initialized:

import (
	"github.com/rdrdr/hamcrest/asserter"
	"github.com/rdrdr/hamcrest/slices"
	. "github.com/rdrdr/hamcrest/core"
	"github.com/rdrdr/hamcrest/strings"
)
var we = asserter.UsingStderr()

type Server struct {
	hostname string
	port uint16
}
var servers = []Server {
	{ "news.foo.com", 8000 },
	{ "news.bar.com", 8888 },
}

func init() {
	ToHostname := Applying(func(s Server) string {
		return s.hostname
	}, "TransformServerToHostname")
	IsInOneOfOurDomains := AnyOf(strings.HasSuffix(".foo.com"),
                                 strings.HasSuffix(".bar.com"))
	we.FailNowUnless(servers,
		slices.EachElem(ToHostname(IsInOneOfOurDomains)))
}

Or use the asserter at runtime to guarantee that a method's preconditions are met:

var IsValidFilenameForTextFile = AllOf(
	strings.HasSuffix(".txt").Comment("Must have .txt extension."),
	Not(strings.HasPattern("[ \\t\\n\\r]")).Comment("whitespace not permitted"),
	Not(strings.HasPattern("[:////\\\\]")).Comment("path separators not permitted"))

func WriteTo(filename string) bool {
	we.AssertThat(filename, IsValidFilenameForTextFile)
	// Use filename here.
}

Or use the asserter with defer to program by contract, knowing that you can later swap out your typical Asserter with an asserter.ThatDoesNothing() to bypass those checks:

IsWellFormedMD5 := AllOf(
	ToLen(EqualTo(32)).Comment("MD5 hashes have length 32"),
	strings.HasPattern("^([0-9][A-F][a-f])+$").Comment("hex digits")))

func MD5(data []byte) (result string) {
	we.AssertNonNil(data)
	defer we.AssertThat(result, IsWellFormedMD5)
		
	// calculate MD5 of data
}

Or use it during development to write your tests in the same file as your code:

func EncodePigLatin(input string) string {
	...implementation...
}

func init() {
	we := asserter.UsingStderr()
	ToEncoded := Applying(EncodePigLatin, "in PigLatin form")
	we.AssertThat("simple", ToEncoded(EqualTo("imple-say")))
	we.AssertThat("Capital"), ToEncoded(EqualTo("Apital-Cay")))
	we.AssertThat("prefix"), ToEncoded(EqualTo("efix-pray")))
	we.AssertThat("oops"), ToEncoded(EqualTo("oops-ay")))
	we.AssertThat("your psychotic y!"),
		ToEncoded(EqualTo("our-yay ychotic-psay y-ay!")))
}

At some future point, this structure will make it easier to cut-and-paste each init() block into your testing suite. While moving the block over, replace:

func init() {
	we := asserter.UsingStderr()
	...

With an Asserter that uses the testing infrastructure, instead:

func Test_EncodePigLatin(t *testing.T) {
	we := asserter.Using(t)
	...

A note on library design:

Hamcrest is designed to be a fluent library. The Go syntax requires external symbols be preceded by their package name, which can lead to occasionally awkward constructions: we.CheckThat("foobar", core.AllOf(strings.HasPrefix("foo"), strings.HasSuffix("bar")))

To avoid this clunkiness, Hamcrest matchers are generated by functions that you can assign to local names: AllOf := core.AllOf StartsWith := strings.HasPrefix EndsWith := strings.HasSuffix we.CheckThat("foobar", AllOf(StartsWith("foo"), EndsWith("bar")))

Performance note:

Note: Hamcrest matchers allocate Description and Result objects to explain in great detail why they did or did not match. However, these objects are lazily evaluated. Users should be generally aware that there is an object allocation cost to using Hamcrest matchers, but there is (generally) no string construction cost unless a Hamcrest Matcher or Result is explicitly asked to self-describe.

Still, users who are particularly sensitive to performance concerns may wish to think carefully before using Hamcrest matchers in performance-critical bottlenecks.

A tour of common matchers

Hamcrest comes with a library of useful matchers. Here are some of the most common ones.

Syntactic sugar

Hamcrest strives to make your tests as readable as possible. For example, the Is matcher is a wrapper that doesn't add any extra behavior to the underlying matcher. The following assertions are equivalent:

we.AssertThat(x, EqualTo(y));
we.AssertThat(x, Is(EqualTo(y)))

Similarly, it is possible to simulate common logical conditions using the logic package for readability:

Custom matchers

Example:

func IsMultipleOf(k int) *base.Matcher {
	match := func(n int) bool {
		return n % k == 0
	}
	return base.NewMatcherf(match, "multiple of %v", k)
}

And used:

we.CheckThat(recordSize, IsMultipleOf(8).Comment(
	"profiling suggests better performance than multiple of 4"))

Or, if you want more control over the error messages:

func IsMultipleOf(k int) *base.Matcher {
	match := func(actual interface{}) *base.Result {
		if n, ok := actual.(int); ok {
			if remainder := n % k; remainder != 0 {
				return base.NewResultf(false,
					"was not a multiple of %v (remainder %v)", k, remainder)
			}
			return base.NewResultf(true, "was a multiple of %v", k)
		}
		return base.NewResultf(false, "was a %T, expected an int!", actual)
	}
	return base.NewMatcherf(match, "multiple of %v", k)
}