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
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
31 changes: 21 additions & 10 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 @@ -59,14 +58,10 @@ import (
// 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 T) {
t.Log("Hello Testo!")
})
}
```

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

Testo also supports suites:

```go
type T struct { *testo.T }

type MySuite struct { testo.Suite[T] }

func (MySuite) TestHello(t T) {
t.Log("Hello from Testo Suite!")
}

func Test(t *testing.T) {
testo.RunSuite(t, new(MySuite))
}
```

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

### Next steps
Expand Down
27 changes: 23 additions & 4 deletions collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,17 +191,36 @@ func (tc *testsCollector[Suite, T]) testName(base string) string {
//nolint:cyclop,funlen,gocognit // splitting it would make it even more complex
func (tc *testsCollector[Suite, T]) Collect(
tb testing.TB,
suite Suite,
) suiteTests[Suite, T] {
tb.Helper()

// special case for [Test] and [RunTest].
if s, ok := any(suite).(singleton[T]); ok {
return suiteTests[Suite, T]{
Regular: []suiteTest[Suite, T]{
{
Name: s.name,
Info: testoreflect.RegularTestInfo{
Name: tc.testName(s.name),
RawBaseName: s.name,
Level: 1,
FuncPC: reflect.ValueOf(s.test).Pointer(),
},
Run: func(_ Suite, t T) { s.test(t) },
},
},
}
}

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 +232,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
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!")
}))
}
```
4 changes: 4 additions & 0 deletions examples/09_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
37 changes: 37 additions & 0 deletions examples/09_suiteless/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//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("Hello from testo!")
})
}

func TestMultiple(t *testing.T) {
t.Run("First test", testo.Test(func(t T) {
t.Log("Hello from the first test!")
}))

t.Run("Second test", testo.Test(func(t T) {
t.Log("Hello from the second test!")
}))
}

func TestMultipleParallel(t *testing.T) {
t.Run("First test", testo.Test(func(t T) {
t.Log("Hello from the first test!")
}).Parallel())

t.Run("Second test", testo.Test(func(t T) {
t.Log("Hello from the second test!")
}).Parallel())
}
55 changes: 55 additions & 0 deletions examples/09_suiteless/output.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
=== RUN TestSimple
=== RUN TestSimple/#00
=== RUN TestSimple/#00/testo!
=== RUN TestSimple/#00/testo!/TestSimple
main_test.go:15: Hello from testo!
--- PASS: TestSimple (0.00s)
--- PASS: TestSimple/#00 (0.00s)
--- PASS: TestSimple/#00/testo! (0.00s)
--- PASS: TestSimple/#00/testo!/TestSimple (0.00s)
=== RUN TestMultiple
=== RUN TestMultiple/First_test
=== RUN TestMultiple/First_test/#00
=== RUN TestMultiple/First_test/#00/testo!
=== RUN TestMultiple/First_test/#00/testo!/First_test
main_test.go:21: Hello from the first test!
=== RUN TestMultiple/Second_test
=== RUN TestMultiple/Second_test/#00
=== RUN TestMultiple/Second_test/#00/testo!
=== RUN TestMultiple/Second_test/#00/testo!/Second_test
main_test.go:25: Hello from the second test!
--- PASS: TestMultiple (0.00s)
--- PASS: TestMultiple/First_test (0.00s)
--- PASS: TestMultiple/First_test/#00 (0.00s)
--- PASS: TestMultiple/First_test/#00/testo! (0.00s)
--- PASS: TestMultiple/First_test/#00/testo!/First_test (0.00s)
--- PASS: TestMultiple/Second_test (0.00s)
--- PASS: TestMultiple/Second_test/#00 (0.00s)
--- PASS: TestMultiple/Second_test/#00/testo! (0.00s)
--- PASS: TestMultiple/Second_test/#00/testo!/Second_test (0.00s)
=== RUN TestMultipleParallel
=== RUN TestMultipleParallel/First_test
=== PAUSE TestMultipleParallel/First_test
=== RUN TestMultipleParallel/Second_test
=== PAUSE TestMultipleParallel/Second_test
=== CONT TestMultipleParallel/First_test
=== RUN TestMultipleParallel/First_test/#00
=== RUN TestMultipleParallel/First_test/#00/testo!
=== RUN TestMultipleParallel/First_test/#00/testo!/First_test
main_test.go:31: Hello from the first test!
=== CONT TestMultipleParallel/Second_test
=== RUN TestMultipleParallel/Second_test/#00
=== RUN TestMultipleParallel/Second_test/#00/testo!
=== RUN TestMultipleParallel/Second_test/#00/testo!/Second_test
main_test.go:35: Hello from the second test!
--- PASS: TestMultipleParallel (0.00s)
--- PASS: TestMultipleParallel/First_test (0.00s)
--- PASS: TestMultipleParallel/First_test/#00 (0.00s)
--- PASS: TestMultipleParallel/First_test/#00/testo! (0.00s)
--- PASS: TestMultipleParallel/First_test/#00/testo!/First_test (0.00s)
--- PASS: TestMultipleParallel/Second_test (0.00s)
--- PASS: TestMultipleParallel/Second_test/#00 (0.00s)
--- PASS: TestMultipleParallel/Second_test/#00/testo! (0.00s)
--- PASS: TestMultipleParallel/Second_test/#00/testo!/Second_test (0.00s)
PASS
ok github.com/ozontech/testo/examples/09_suiteless 0.665s
2 changes: 1 addition & 1 deletion options.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ var (

// Options appends given options to the global options.
//
// Global options are prepended to each [RunSuite] call.
// Global options are prepended to each [RunSuite] & [RunTest] call.
//
// func init() {
// testo.Options(myplugin.OutputDir("..."))
Expand Down
Loading