Migration guide
July 26, 2026 ยท View on GitHub
How to move existing tests to Testo - from testify or from allure-go.
Both migrations can be done incrementally: Testo tests are regular
go test tests, so old and new suites run side by side in the same
package with no interference. Migrate one suite at a time.
Migrating from Testify
Suites and hooks
| Testify | Testo |
|---|---|
suite.Suite (embedded) | testo.Suite[T] (embedded) |
suite.Run(t, new(S)) | testo.RunSuite(t, new(S)) |
func (s *S) SetupSuite() | func (*S) BeforeAll(t T) |
func (s *S) SetupTest() | func (*S) BeforeEach(t T) |
func (s *S) TearDownTest() | func (*S) AfterEach(t T) |
func (s *S) TearDownSuite() | func (*S) AfterAll(t T) |
func (s *S) TestFoo() | func (*S) TestFoo(t T) |
s.T() | t (passed to every method) |
s.Run(name, func()) | testo.Run(t, name, func(t T)) |
func (s *S) BeforeTest(suiteName, testName string) | BeforeEach(t T) + t.Name() (same for AfterTest) |
SetupSubTest() / TearDownSubTest() | BeforeEachSub/AfterEachSub plugin hooks (no suite-level equivalent) |
go test -run TestSuite -testify.m TestFoo | go test -run TestSuite -testo.m TestFoo |
Note
t.Name() returns the full path (Test/MySuite/TestFoo),
not the bare method name - compare with strings.HasSuffix, not ==.
And if a suite uses both SetupTest and BeforeTest,
merge them into one BeforeEach (testify runs SetupTest first; keep that order).
One caveat: testify method filters like -run 'TestMySuite/TestFoo'
stop matching after migration, because Testo inserts a hidden testo!
level into test names. Update CI filters to use -testo.m. See
how to run and skip specific tests.
T here is your own type built on *testo.T - in the simplest case
just an alias:
type T = *testo.T
See the tutorial for defining a T with plugins.
Assertions
testify's require and assert packages work with Testo unchanged,
because testo.T implements testing.TB:
// testify/suite:
func (s *MySuite) TestFoo() {
s.Require().NoError(err)
s.Equal(want, got) // shorthand from the embedded suite.Suite
}
// Testo:
func (*MySuite) TestFoo(t T) {
require.NoError(t, err)
assert.Equal(t, want, got)
}
Testo only replaces testify's suite package. Keep using require
and assert.
Suite state
Testify suites keep per-test state in suite struct fields.
A Testo suite is also a single instance, so shared fields still work.
But writes to shared fields from parallel tests are a data race -
run migrated packages with -race before enabling t.Parallel().
For per-test state, prefer fixtures
(methods on your T) or plugin fields. Both get a fresh instance
for each test and are safe with parallel tests.
Parallelism
Testify's suite does not support t.Parallel() (issue #934).
With Testo, call t.Parallel() in any test - hooks handle parallel tests correctly.
See how to write parallel tests.
Migrating from Allure-Go
Testo replaces Allure-Go at Ozon.
Reporting is now a separate plugin, not part of the framework.
So you need two pieces: github.com/ozontech/testo and the
testo-allure plugin.
import (
"github.com/ozontech/testo"
allure "github.com/ozontech/testo-allure"
)
type T struct {
*testo.T
*allure.PluginAllure
}
Concept mapping
| allure-go | Testo (+ testo-allure) |
|---|---|
suite.Suite | testo.Suite[T] |
suite.RunSuite(t, new(S)) (or runner.NewSuiteRunner(...)) | testo.RunSuite(t, new(S)) |
BeforeAll(t provider.T) | BeforeAll(t T) (same for the other hooks) |
func (s *S) TestFoo(t provider.T) | func (*S) TestFoo(t T) |
t.WithNewStep("name", func(ctx provider.StepCtx) {...}) | allure.Step(t, "name", func(t T) {...}) |
t.NewStep("name") | allure.Step(t, "name", func(T) {}) |
t.Title, t.Epic, t.Feature, t.Story, t.Tags, t.Severity, t.Owner, t.ID | same-named methods added to T by PluginAllure |
t.Link(...), t.TmsLink(...), t.TmsLinks(...) | t.Links(...) with the allure.NewLink/allure.TMS/allure.Issue constructors |
t.WithNewAttachment(name, mimeType, content), t.WithAttachments(...) | t.Attach(name, allure.Bytes(...)) or t.Attach(name, allure.File(...)) |
table tests (ParametrizedSuite / TableTestXxx methods) | single struct-typed parameter, see table tests |
t.XSkip() | t.XFail() via the xfail plugin from testo-toppings (semantics differ slightly - check its README) |
Steps
Steps are sub-tests. Two ways to create them:
testo.Run(t, "name", func(t T) {...})- the framework primitive.t.Fatalinside stops only the sub-test, not the outer test.allure.Step(t, "name", func(t T) {...})- the testo-allure helper. Same astesto.Run, but a fatal failure propagates to the parent test and stops it - matchingWithNewStepsemantics from allure-go.
Use allure.Step instead of WithNewStep.
Nested steps (sCtx.WithNewStep, sCtx.NewStep) become nested
allure.Step(t, ...) calls.
Assertions sCtx.Assert() and sCtx.Require() become t.Assert() & t.Require(), which PluginAllure adds to T.
For the full testo-allure API (output directory, labels, attachments and so on), see the testo-allure documentation.