Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ linters:
- thelper

settings:
funlen:
lines: 70
gosec:
excludes:
- G304
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added

- Ability to run tests without suites.

### Changed

- `testo.Options` now returns an empty struct to enable `var _ = testo.Options(...)` usage.
Expand Down
22 changes: 8 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
[![Code Coverage](https://github.com/ozontech/testo/raw/gh-pages/coverage.svg?raw=true)](https://ozontech.github.io/testo/coverage.html)
[![Quality Assurance](https://github.com/ozontech/testo/actions/workflows/qa.yml/badge.svg)](https://github.com/ozontech/testo/actions/workflows/qa.yml)

Testo is a modular testing framework for Go built on top of `testing.T`.
It is focused on suite-based tests and has an extensive plugin system.
Testo is a modular testing framework for Go built on top of `testing.T`
with an extensive plugin system.

> Testo (/tɛstɒ/) is a play on words "test" and "тесто", meaning "dough".
> Just like you can cook anything from dough, you can test anything with Testo!
Expand Down Expand Up @@ -46,7 +46,6 @@ go get github.com/ozontech/testo
Your first test with Testo:

```go
// file: main_test.go
package main

import (
Expand All @@ -55,18 +54,10 @@ import (
"github.com/ozontech/testo"
)

// A special construct that describes what plugins to use.
// Here we use the base T without plugins.
type T struct { *testo.T }

type Suite struct{ testo.Suite[T] }

func (Suite) TestHelloWorld(t T) {
t.Log("hello from testo!")
}

func Test(t *testing.T) {
testo.RunSuite(t, new(Suite))
testo.RunTest(t, func(t *testo.T) {
t.Log("Hello, Testo!")
})
}
```

Expand All @@ -76,6 +67,9 @@ And run it with `go test` as usual:
go test .
```

But there is more!
Testo supports suites, parametrized tests & plugins, see [Next steps](#next-steps).

See also [VS Code extension for Testo](#vs-code-extension).

### Next steps
Expand Down
39 changes: 33 additions & 6 deletions collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,14 +153,20 @@ type suiteTests[Suite suite[T], T CommonT] struct {
type annotatedSuiteTest[Suite suite[T], T CommonT] struct {
suiteTest[Suite, T]

// Options to pass specifically for this test.
Options []testoplugin.Option

Configure func(*testoT)
}

// Collect all suite tests.
//
// Suite instance is required here to get
// parameter cases (CasesXXX funcs), not to invoke the actual tests.
func (st suiteTests[Suite, T]) Collect(s Suite) []annotatedSuiteTest[Suite, T] {
func (st suiteTests[Suite, T]) Collect(
s Suite,
name func(string) string,
) []annotatedSuiteTest[Suite, T] {
tests := make([]annotatedSuiteTest[Suite, T], 0, len(st.Regular))

for _, r := range st.Regular {
Expand All @@ -176,6 +182,28 @@ func (st suiteTests[Suite, T]) Collect(s Suite) []annotatedSuiteTest[Suite, T] {
tests = append(tests, cases...)
}

// special case for [Test] and [RunTest].
//
// NOTE(metafates): future "special" suites should be handled here in a type switch.
if s, ok := any(s).(singleton[T]); ok {
tests = append(tests, annotatedSuiteTest[Suite, T]{
suiteTest: suiteTest[Suite, T]{
Name: s.name,
Info: testoreflect.RegularTestInfo{
Name: name(s.name),
RawBaseName: s.name,
Level: 1,
FuncPC: reflect.ValueOf(s.test).Pointer(),
},
Run: func(_ Suite, t T) { s.test(t) },
},
Options: s.options,
Configure: func(tt *testoT) {
tt.propagateParallel = true
},
})
}

return tests
}

Expand All @@ -196,12 +224,12 @@ func (tc *testsCollector[Suite, T]) Collect(

cases := suiteCasesOf[Suite](tb)

suite := reflect.TypeFor[Suite]()
suiteTyp := reflect.TypeFor[Suite]()

var tests suiteTests[Suite, T]

for i := range suite.NumMethod() {
method := suite.Method(i)
for i := range suiteTyp.NumMethod() {
method := suiteTyp.Method(i)

if !isTest(method.Name, "Test") {
continue
Expand All @@ -213,7 +241,7 @@ func (tc *testsCollector[Suite, T]) Collect(
//nolint:lll // it's a long message
tb.Fatalf(
"testo: wrong signature for (%[1]s).%[2]s, must be: func (%[1]s).%[2]s(%[3]s) or func (%[1]s).%[2]s(%[3]s, struct{...})",
suite,
suiteTyp,
method.Name,
reflect.TypeFor[T](),
)
Expand Down Expand Up @@ -307,7 +335,6 @@ type suiteTestParametrized[Suite suite[T], T CommonT] struct {
Tests func(Suite) []annotatedSuiteTest[Suite, T]
}

//nolint:funlen // no way to reduce length without losing readability
func (tc *testsCollector[Suite, T]) newParametrizedTest(
method reflect.Method,
cases map[string]suiteCase[Suite, T],
Expand Down
62 changes: 56 additions & 6 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,64 @@
// "github.com/ozontech/testo"
// )
//
// type T struct { *testo.T }
// func Test(t *testing.T) {
// testo.RunTest(t, func(t *testo.T) {
// t.Log("Hello, Testo!")
// })
// }
//
// # Plugins
//
// Plugins are the core feature of Testo.
// Plugins can generate reports, add custom methods to T,
// override built-in methods, plan test execution and more.
//
// Plugins are installed by defining our own T with embedded [T] and plugins:
//
// type T struct {
// *testo.T
// *myplugin.PluginFoo
// *myplugin.PluginBar
// }
//
// func Test(t *testing.T) {
// testo.RunTest(t, func(t T) {
// t.Log("Hello, Testo!")
// })
// }
//
// Notice that we now use our T instead of *testo.T in a test.
//
// # Suites & Standalone Tests
//
// Testo supports several ways to run tests.
//
// Suites:
//
// type Suite struct { testo.Suite[T] }
//
// func (Suite) Test(t T) { t.Log("Hello, world!") }
// func (Suite) TestFoo(t T) { t.Log("Foo") }
// func (Suite) TestBar(t T) { t.Log("Bar") }
//
// func Test(t *testing.T) {
// testo.RunSuite(t, new(Suite))
// }
//
// Standalone:
//
// func TestFoo(t *testing.T) {
// testo.RunTest(t, func(t T) {
// t.Log("Foo")
// })
// }
//
// func Test(t *testing.T) { testo.RunSuite(t, new(Suite)) }
// func Test(t *testing.T) {
// t.Run("Foo", testo.Test(func(t T) {
// t.Log("Foo")
// }))
//
// Notice the `Test(t *testing.T)` - since [RunSuite] requires
// an instance of `testing.T` we must declare a regular go test first,
// and only inside it we will be able to actually call our Testo suite.
// t.Run("Bar", testo.Test(func(t T) {
// t.Log("Bar")
// }))
// }
package testo
38 changes: 38 additions & 0 deletions docs/how-to.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,44 @@

Learn how to use the features of Testo.

## How to write parametrized tests

Parametrized tests are defined as regular tests with a second argument:

```go
func (*Suite) TestFoo(t *testo.T, p struct{ Name string; Age int }) {
t.Logf("Using name=%q and age=%d", p.Name, p.Age)
}
```

To define all possible parameter values create a special `CasesXxx` method in a suite:

```go
func (*Suite) CasesName() []string {
return []string{"John", "Joe"}
}

func (*Suite) CasesAge() []int {
return []int{18, 60, 6}
}
```

> [!TIP]
> `CasesXxx` are invoked *after* `BeforeAll` hook.

Field names used in a `struct{ Name string; Age int}` must be equal to existing `CasesXxx` functions.

Given that, test `TestFoo` will be invoked with all possible combinations of names and ages:

```python
TestFoo(name=John, age=18)
TestFoo(name=John, age=60)
TestFoo(name=John, age=6)
TestFoo(name=Joe, age=18)
TestFoo(name=Joe, age=60)
TestFoo(name=Joe, age=6)
```

## How to write parallel tests

You can use your regular `t.Parallel` method to mark a test as parallel.
Expand Down
37 changes: 37 additions & 0 deletions docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ type T = *testo.T

Now we need a Suite. A Suite must "inherit" `testo.Suite[T]` by embedding it.

> [!NOTE]
> It's possible to run tests without suites, more on that in [later](#running-tests-without-suites).

```go
type Suite struct{ testo.Suite[T] }

Expand Down Expand Up @@ -324,3 +327,37 @@ func (*Suite) TestBoom(t T) {
>
> Pointers allow plugins to share their state with other plugins,
> by pointing to the same memory location through pointers.

## Running tests without suites

It's possible:

```go
type T struct{
*testo.T
*ReverseTestsOrder
*OverrideLog
*AddNewMethods
*Timer
}

func TestFoo(t *testing.T) {
testo.RunSuite(t, func(t T) {
t.Log("Hello from testo!")
})
}
```

Or, if you need to run several tests from a single "real" test:

```go
func TestFoo(t *testing.T) {
t.Run("FirstTest", testo.Test(func(t T) {
t.Log("1!")
}))

t.Run("SecondTest", testo.Test(func(t T) {
t.Log("2!")
}))
}
```
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
--- PASS: Test/Suite/testo! (0.00s)
--- PASS: Test/Suite/testo!/TestMath (0.00s)
PASS
ok github.com/ozontech/testo/examples/01_minimal 0.212s
ok github.com/ozontech/testo/examples/01_suite 0.212s
4 changes: 4 additions & 0 deletions examples/01_suiteless/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
MAKEFLAGS += --always-make

test:
go test . -v -tags example -count=1
41 changes: 41 additions & 0 deletions examples/01_suiteless/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//go:build example

package main

import (
"testing"

"github.com/ozontech/testo"
)

type T = *testo.T

func TestSimple(t *testing.T) {
testo.RunTest(t, func(t T) {
t.Log(t.Name())
})
}

func TestMultiple(t *testing.T) {
t.Run("First test", testo.Test(func(t T) {
t.Log(t.Name())
}))

t.Run("Second test", testo.Test(func(t T) {
t.Log(t.Name())
}))
}

func TestMultipleParallel(t *testing.T) {
t.Run("First test", testo.Test(func(t T) {
t.Parallel()

t.Log("Hello from the first test!")
}))

t.Run("Second test", testo.Test(func(t T) {
t.Parallel()

t.Log("Hello from the second test!")
}))
}
Loading