A simple test framework for Go, intended for TDD beginners and novice developers.
This is a test:
package calculator
import (
"github.com/bearstonedev/gotest"
"testing"
)
func Test_ShouldCompareTwoNumbers(t *testing.T) {
calculator := MyCalculator{}
I := gotest.Expec(t)
I.Expect(calculator.Add(1, 1)).ToBe(2)
}
Note the readability; the test documents the expected result. Here's how the same test would be written in standard Go:
package calculator
import "testing"
func Test_ShouldCompareTwoNumbers(t *testing.T) {
calculator := MyCalculator{}
t.Parallel()
t.Run("", func(tt *testing.T) {
tt.Parallel()
expected := 2
actual := calculator.Add(1, 1)
if actual != expected {
tt.Fatalf("Expected %v to be %v", expected, actual)
}
})
}
You can write multiple expectations in one test ...
package calculator
import (
"github.com/bearstonedev/gotest"
"testing"
)
func Test_ShouldCompareTwoNumbers(t *testing.T) {
calc := CreateCalculator()
I := gotest.Expec(t)
I.Expect(calc.IsGreaterThan(2, 1)).ToBe(true)
I.Expect(calc.IsGreaterThan(1, 2)).ToBe(false)
}
...and you can name them, if you want to:
package calculator
import (
"github.com/bearstonedev/gotest"
"testing"
)
func Test_ShouldCompareTwoNumbers(t *testing.T) {
calc := CreateCalculator()
I := gotest.Expec(t)
I.Expect(calc.IsGreaterThan(2, 1)).As("2 > 1").ToBe(true) // Test will be named: I_expect_2_>_1_to_be_true
I.Expect(calc.IsGreaterThan(1, 2)).As("1 > 2").ToBe(false) // Test will be named: I_expect_1_>_2_to_be_false
}
Each expectation is run as a subtest of the containing test.
📝 Make it easier to write descriptive specifications of behaviour (BDD) by reducing boilerplate test code.
- All tests are run in parallel, both to reduce boilerplate and to help enforce independent tests.
t.FailNow
,t.Log
, etc. are abstracted.
Each "expectation" is run as a distinct, parallel test. This helps ensure:
- Tests are temporally decoupled, meaning they don't need to be run in a specific order;
- Tests are distinct, making it easier to localize a specific failure; and,
- Tests are safe because low-level test implementation details are managed on your behalf.
- Standard
go
commands should work as expected to automate testing. - Example:
go test -run 'SomeTestSuite/specific_scenario' -v
should produce the same result as with native Go tests.
Test are written to specify expected behaviour; we use them to measure and evaluate the software we write. Tests should be written in advance to help guide design and implementation; this is known as test-driven development (TDD).
A test is a function which invokes a part of the software we're writing; this can be a function or method, a module, or even an entire application. The test specifies the expected inputs (values we provide to our software) and outputs ( values we receive from our software) for a specific behaviour. A collection of tests, known as a test suite, helps us identify problems with our design and/or implementation during development when those problems are easiest to address.
This function should follow standard Go test conventions.
package pizza_ordering
import "testing"
func TestOrderingADeluxePizza(t *testing.T) {}
This is the "starting point" for writing tests, and will be used to write one or more test cases; these tests should be cohesive (related to each other).
package pizza_ordering
import (
. "github.com/bearstonedev/gotest"
"testing"
)
func TestOrderingADeluxePizza(t *testing.T) {
I := Expec(t)
}
Depending on what you're testing, you might have a type or module to be tested; this is colloquially known as the * system-under-test* (SUT). You should set it up with any relevant preconditions for the scenario you're testing.
package pizza_ordering
import (
. "github.com/bearstonedev/gotest"
"testing"
)
func TestOrderingADeluxePizza(t *testing.T) {
I := Expec(t)
sut := &PizzaOrderMaker{location: "New York"}
}
This will compare the expected output to the output produced by your program.
package pizza_ordering
import (
. "github.com/bearstonedev/gotest"
"testing"
)
func TestOrderingADeluxePizza(t *testing.T) {
I := Expec(t)
sut := &PizzaOrderMaker{location: "New York"}
expected := &PizzaOrder{
toppings: []string{"tomato", "green olives", "red peppers"},
locale: "New York",
}
actual := sut.OrderPizza("tomato", "green olives", "red peppers")
I.Expect(actual.toppings).ToBe(expected.toppings)
I.Expect(actual.locale).ToBe(expected.locale)
}
Now that you have a failing test, you should implement the changes needed to make that test pass. Try to avoid writing many tests upfront, or writing tests after-the-fact; writing tests and implementation iteratively will "drive" the design of the software you're building in small increments.
Refactoring (noun): a change made to the internal structure of software to make it easier to understand and cheaper to modify without changing its observable behavior.
Refactoring (verb): to restructure software by applying a series of refactorings without changing its observable behavior.
https://martinfowler.com/bliki/DefinitionOfRefactoring.html
Refactoring must be done frequently to minimize accidental complexity. When building software, try working in cycles of "Red-Green-Refactor":
- Write a failing test;
- Make that test pass;
- Refactor the test and the implementation.
Working examples are shown here: gotest/examples.
(The examples are not bundled with the published module.)
The currently published version can be found here.