go-testfixtures/testfixtures: Rails style fixture files for testing Go against a real database
Ruby on Rails like test fixtures for Go. Write tests against a real database
At a glance
- What is it?
- testfixtures loads YAML sample data into a real database before each test, in the style Ruby on Rails popularised, with per table files, automatic type conversion for JSON, binary and timestamp columns, and a RAW= escape hatch for raw SQL expressions. Its first line of documentation is a warning that it wipes the database before loading.
- Who is it for?
- Adopt testfixtures if your Go service has SQL access that mocks would hide, since the package's argument is exactly that, running against a real database catches the production bugs a mock cannot, and the fixture format is cheap to maintain.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Why fixtures instead of mocks
testfixtures is a Go package that gives you the Ruby on Rails approach to testing database applications, where sample data lives in fixture files and the test database is cleaned and reloaded before each test. The stated aim is to make writing functional tests for web applications in Go easier.
The reasoning behind it is worth stating, because it is an opinion as much as a feature. The idea is to run tests against a real database instead of relying on mocks, which is described as boring to set up and as something that may lead to production bugs not being caught in the tests.
That argument has teeth. A mocked repository layer will happily return a shape your SQL never produces, and the class of bug that comes from the gap between an ORM model and the actual table is exactly the class that unit tests with mocks tend to miss. The price is that every test now needs a database, a clean schema and a reload, which is real infrastructure, and the package's design is a way of making that price bearable rather than a way of avoiding it.
The package is a v3 module, `github.com/go-testfixtures/testfixtures/v3`, MIT licensed, and it depends on very little, with `goccy/go-yaml` for parsing and `golang.org/x/sync`, on Go 1.25.9.
The first thing the documentation says is a warning
Before anything else, the README carries a warning: this package will wipe the database data before loading the fixtures, it is supposed to be used on a test database, and you should double check whether you are running it against the correct database.
That warning is not boilerplate, because the mechanism is a truncate and reload of the tables your fixtures cover, which is destructive by design. The example code in the README repeats the point as a comment in the connection setup:
// Open connection to the test database.
// Do NOT import fixtures in a production database!
// Existing data would be deleted.
db, err = sql.Open("postgres", "dbname=myapp_test")The package anticipates the obvious accident, and there is a security check for it, which is covered further down. The design position is that the tool is safe in the hands of someone who reads one line of documentation, and dangerous in the hands of someone who does not, which is a reasonable position for a package whose whole purpose is to delete your data and replace it.
There is also a note that there are options not described on the README page, and the recommendation to check the package documentation on pkg.go.dev, which is where the full option list lives.
One file per table, and the file name is the table name
The layout is the Rails layout. You create a folder for the fixture files, each file contains data for a single table, and the file is named `<table_name>.yml`:
myapp/
myapp.go
myapp_test.go
...
fixtures/
posts.yml
comments.yml
tags.yml
posts_tags.yml
...The file itself is a YAML sequence of records, one per row, and it can hold as many records as you want:
# comments.yml
- id: 1
post_id: 1
content: A comment...
author_name: John Doe
author_email: [email protected]
created_at: 2020-12-31 23:59:59
updated_at: 2020-12-31 23:59:59The convention carries the usual benefits and one trap. The benefit is that the file name tells you the table without opening it, and a fixture set reads like a schema. The trap is that the file name is not a suggestion, it is the insert target, so renaming a table means renaming the file, and a mismatch fails at load time rather than silently.
For fixtures that need to cover several tables at once, the `FilesMultiTables` option takes files whose contents are keyed by table name, so the file name no longer determines the table.
Type conversion is where fixtures get subtle
The package converts values on the way in, and the rules are documented, which is good, because they are the part that causes surprising failures.
A YAML object or array is converted to JSON, and it is stored on a native JSON type such as JSONB on PostgreSQL and CockroachDB, or as a TEXT or VARCHAR column on other databases. So a nested record becomes a JSON blob rather than a set of columns, which is convenient for a Rails style `post_attributes` column and useless if you expected normalisation.
Binary columns are represented as hexadecimal strings that should start with `0x`, and they are automatically converted to `[]byte`:
- id: 1
binary_column: 0x1234567890abcdefString values matching date and time formats, with examples like `2025-08-15`, `20250815` and `15/08/2025`, are automatically converted to `time.Time`, and the supported formats are listed in the package's time.go. The `RAW=` prefix prevents the conversion:
- id: 1
created_at: 2025-08-15 # Will be converted to time.Time
updated_at: RAW=20250815 # Will remain as stringAnd `RAW=` also gives you raw SQL, which is what you need when a column is generated by the database:
- id: 1
uuid_column: RAW=uuid_generate_v4()
postgis_type_column: RAW=ST_GeomFromText('params...')
created_at: RAW=NOW()
updated_at: RAW=NOW()So there is one escape hatch with two uses, keeping a value as a string and letting the database compute it. The automatic date conversion is the one to think about, because a column that genuinely holds the string `20250815` will be turned into a timestamp unless you remember the prefix.
The loader API and its three ways of pointing at files
The loader is built with an options pattern, and the three options you always need are the database, the dialect and where the files live:
fixtures, err = testfixtures.New(
testfixtures.Database(db), // You database connection
testfixtures.Dialect("postgres"), // Available: "postgresql", "timescaledb", "mysql", "mariadb", "sqlite" and "sqlserver"
testfixtures.Directory("testdata/fixtures"), // The directory containing the YAML files
)The dialect list in that comment is the supported set, and it is longer than the obvious ones, with TimescaleDB, MariaDB and SQL Server alongside PostgreSQL, MySQL and SQLite.
There are then two more precise ways to select files. `Files` takes an explicit list of YAML files, which is what you want when a test needs only two tables:
fixtures, err := testfixtures.New(
testfixtures.Database(db),
testfixtures.Dialect("postgres"),
testfixtures.Files(
"fixtures/orders.yml",
"fixtures/customers.yml",
),
)And `Paths` takes paths that may be a file or a directory, and when it is a directory the YAML files inside it are searched:
fixtures, err := testfixtures.New(
testfixtures.Database(db),
testfixtures.Dialect("postgres"),
testfixtures.Paths(
"fixtures/orders.yml",
"fixtures/customers.yml",
"common_fixtures/users"
),
)The distinction between the two is the one that catches people. `Files` and `FilesMultiTables` name files explicitly, while `Paths` may name directories, and the package documentation for the options not covered in the README is where the remaining behaviour lives.
The guard rail that refuses to load into a non test database
The security check is the part of the design that deserves praise, because it turns a dangerous default into a refusal.
In order to prevent you from accidentally wiping the wrong database, the package will refuse to load fixtures if the database name, or the database filename for SQLite, does not contain the word `test`. The check exists because the failure mode it prevents is catastrophic and quiet: a test suite pointed at a production connection would empty your tables and then repopulate them with three rows of YAML.
So a test database whose name does not contain `test` simply will not work, and the documentation describes a way to disable the check for the cases where the naming convention does not fit your setup, which is the correct way to handle it, since the check is a speed bump rather than a security boundary and an operator who deliberately turns it off has at least had to decide to.
The practical advice for a team is to name the connection string after the check, so that the guard rail and your conventions reinforce each other rather than fighting.
Where this approach stops working
The first limit is the obvious one: you need a database. A test suite using this package needs a reachable PostgreSQL, MySQL, SQLite or SQL Server instance, which means either a shared test database, a container per run, or something in between, and that is operational surface that a mocked repository layer does not have. The package does not provision the database for you, and the repository carries `dbtests/` and a `cmd/` directory and a `go.work` workspace, which suggests the project's own integration testing work is where the complexity lives.
The second limit is isolation. The package cleans and reloads, it does not wrap each test in a transaction and roll it back, so parallel tests against one database will interfere with each other. Test functions in the example each call a `prepareTestDatabase()` helper before running, which is a serialisation strategy rather than an isolation strategy, and any project that wants `t.Parallel()` has to solve the database problem separately.
The third is the type conversion behaviour, which is helpful by default and wrong in the edges. Automatic date parsing, automatic JSON conversion for nested structures and automatic hex to byte conversion are all conveniences that silently reinterpret a value, and the `RAW=` prefix is the escape hatch for each of them. A project with unusual column types, generated columns or extension types such as PostGIS will spend its first afternoon discovering which prefix it needs.
The fourth is the alternative approach worth naming. If your application already has a repository layer that can be swapped for an in-memory implementation, or if you generate your database access from a schema with a query builder, then the same argument that testfixtures makes against mocks can be made in the opposite direction, and a suite that runs without a database at all is faster and easier to parallelise. The deciding factor is usually whether your production data access path is SQL enough for the difference between a mock and a real table to matter.
Editorial conclusion
Adopt testfixtures if your Go service has SQL access that mocks would hide, since the package's argument is exactly that, running against a real database catches the production bugs a mock cannot, and the fixture format is cheap to maintain. Do not adopt it if your tests are already hermetic with an in-memory repository layer, since a real database per run is a cost those suites do not pay, and do not adopt it if you need transaction isolation, because the package loads and cleans rather than rolling back. Verify first that the guard rail that refuses to load fixtures unless the database name contains 'test' is compatible with your test database naming, that your driver and dialect are among the supported ones, and that the type conversion rules for timestamps and JSON columns match what your columns actually expect.
Frequently asked questions
How do I create fixtures for testfixtures?
Create a directory with one YAML file per table, named `<table_name>.yml`, each file holding a sequence of records. The directory is then passed to the loader with the `Directory` option, or individual files with `Files`, or a mix of files and directories with `Paths`.
Is it safe to run testfixtures against my database?
Not against a production database. The package wipes the data before loading fixtures, and the README warns about it twice. As a safeguard it refuses to load fixtures if the database name, or the SQLite database filename, does not contain `test`, and the package documentation describes how to disable that check if your naming does not fit.
How do I insert raw SQL or keep a value as a string in a fixture?
Prefix the value with `RAW=`. For raw SQL that gives expressions like `uuid_column: RAW=uuid_generate_v4()` or `created_at: RAW=NOW()`, and for a value that should stay a string it prevents the automatic conversion to `time.Time`, for example `updated_at: RAW=20250815`.
Which databases does testfixtures support?
The dialect option accepts `postgresql`, `timescaledb`, `mysql`, `mariadb`, `sqlite` and `sqlserver`. The package works through `database/sql`, so you open the connection with your own driver and pass it in with the `Database` option.
Can one fixture file contain rows for several tables?
Yes, with the `FilesMultiTables` option the file name does not determine the table, and the file content is keyed by table name, with each table holding its own list of records.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/go-testfixtures-testfixtures)Was this useful?