Quick Start
This is a quick start guide to get you up and running with FizzBee’s Model Based Testing for Go. Quick start guide for Java and Rust are coming soon.` We’ll cover additional details and more complex examples in later documents.
In this guide, we’ll cover:
- Installing FizzBee and FizzBee-MBT
- Create and validate a simple FizzBee model
- Implement test adapter in Go.
- Test Driven Development (TDD) workflow without writing a single test.
- Test single threaded and multi-threaded behavior.
- Integration test with an external database.
- Checking code coverage of the tests.
Table of Contents
FizzBee has two components:
- the FizzBee model checker for validating the system design
- the FizzBee Model Based Testing (MBT) tool to test your implementation.
On Mac with homebrew:,
brew tap fizzbee-io/fizzbee
brew install fizzbee
brew tap fizzbee-io/fizzbee-mbt
brew install fizzbee-mbt
Manual installation
Alternatively, you can download and install them manually. You’ll need both the FizzBee Model Checker and the FizzBee Model Based Testing binaries.
For FizzBee Model Checker, the binaries are available at, https://github.com/fizzbee-io/fizzbee/releases
For FizzBee Model Based Testing, the binaries are available at, https://github.com/fizzbee-io/fizzbee-mbt-releases/releases
Make sure to add both the un-tarred (tar -xzf filename.tar.gz
) directories to your PATH.
Most likely you will use FizzBee-MBT in an existing project. But for this quick start guide, let’s create a new Go project.
# cd to a directory where you want to create the project.
mkdir fizzbee-mbt-quickstart
cd fizzbee-mbt-quickstart
go mod init fizzbee-mbt-quickstart
In most projects, we would keep the model in a separate directory under specs
or models
directory.
mkdir -p specs/simple-counter
vim specs/simple-counter/counter.fizz # Or any editor of your choice
|
|
You could run and explore the model with the FizzBee online playground at https://fizzbee.io/play
Run the above code in the playground.
“Run in playground” will open the snippet in the playground.
Then, click “Enable whiteboard” checkbox and click Run.
The screenshot of the playground highlighting the Enable whiteboard and Run steps
When you run it, you’ll see an The screenshot of the playground highlighting the Explorer link to open the Explorer viewExplorer
link. Click you’ll see an explorer.
For model based testing, you’ll need to run the model checker locally.
fizz specs/simple-counter/counter.fizz
The output states would be stored in a directory specs/simple-counter/out/run_<timestamp>
.
Now, run the scaffolding command to generate the test skeleton and the adapter code.
fizz mbt-scaffold \
--lang go \
--go-package counter \
--gen-adapter \
--out-dir fizztests/ \
specs/simple-counter/counter.fizz
Optionally, format the generated code with go fmt
for consistent code style.
go fmt fizztests/*
This will generate 3 files under fizztests
directory.
File | Description | Edit/Implement |
---|---|---|
counter_test.go | The test starter code that can be started with go test . |
Do not edit |
counter_interfaces.go | The interfaces for the roles and actions defined in the model. | Do not edit |
counter_adapters.go | The scaffolding for the adapter implementing the interface. | Implement |
Take a quick look at the counter_interfaces.go
file and the counter_adapters.go
.
First step, you’ll need to go get
the fizzbee-mbt package.
go get github.com/fizzbee-io/fizzbee/mbt/lib/go
FizzBee MBT runs with two processes.
- fizzbee-mbt-server does the validation.
- FizzBee MBT test runner runs the tests.
In one terminal, run the server. Use this path to the states file from the model checker output.
fizzbee-mbt-server --states_file specs/simple-counter/out/run_2025-09-26_14-15-41 > /tmp/server.log
The output will be something like this.
2025/09/26 15:05:26 Loaded 8 nodes and 24 links
2025/09/26 15:05:26 State Space Options: options:{max_actions:100 max_concurrent_actions:2 crash_on_yield:true} deadlock_detection:true
2025/09/26 15:05:26 FizzMo server listening on :50051
In another terminal, run the tests go test ./fizztests
.
go test ./fizztests
2025/09/26 15:11:22 Init RPC returned error: Failed to get roles: not implemented
2025/09/26 15:11:22 Init failed: Init failed: Failed to get roles: not implemented
2025/09/26 15:11:22 Runner exited with error: exit status 1
2025/09/26 15:11:22 exit status 1
FAIL fizzbee-mbt-quickstart/fizztests 0.353s
FAIL
The test fails because we haven’t implemented the adapter yet.
Open the fizztests/counter_adapters.go
file. And look for the GetRoles
method.
|
|
For the test runner to invoke the actions on the roles, it needs to know what roles are there. This is the critical step in the adapter implementation. In our model, there is a single instance of the Counter. So, we need to return a map with a single entry.
|
|
So, to finally fix, this. Make these 3 changes.
- Add a field
counterRole
of type*CounterRoleAdapter
to theCounterModelAdapter
struct.
|
|
- Initialize the field in the
Init
method.
|
|
- Return the field in the
GetRoles
method.
|
|
Now, run the tests again.
go test ./fizztests
ok fizzbee-mbt-quickstart/fizztests 1.985s
The tests passed. Even though we don’t have an implementation.
Before we work it out, let’s try verbose logging to see what’s happening. adding -v
flag to go test
.
go test -v ./fizztests
=== RUN TestCounter
Sequential tests succeeded! Number of sequential runs: 1000
Parallel tests succeeded! Number of parallel runs: 1000
2025/09/27 14:24:48 Test executor shut down gracefully.
--- PASS: TestCounter (1.81s)
PASS
ok fizzbee-mbt-quickstart/fizztests 1.812s
Here, you can see, FizzBee tried 1000 sequential and 1000 parallel runs.
So why didn’t the tests fail?
It is because,
we haven’t implemented the actions yet, and the default implementation for the actions
return mbt.ErrNotImplemented
error. (For Java, it will throw ActionNotImplementedException)
FizzBee treats actions that return mbt.ErrNotImplemented
as disabled tests.
In the adapter file, there is a GetTestOptions
method with this default implementation.
|
|
For now, let’s disable the parallel tests. We will explore parallel tests in a later document.
|
|
In fizztests/counter_adapters.go, implement the ActionGet
method in CounterModelAdapter
.
Replace this code:
|
|
With this code:
|
|
Similarly, implement the ActionInc
method.
|
|
Now, run go test ./fizztests
again.
go test ./fizztests
Validation failed: Failed: Counter#0.Get, expected: [int_value:2], actual: [int_value:0]
0: Counter#0.Inc()
1: Counter#0.Inc()
==> 2: Counter#0.Get() -> 0
Return value mismatched
Expected:
2: Counter#0.Get() -> 2
Test failure after 1 trace(s).
To retry the same trace, use: --seq-seed=1759010359241424000
2025/09/27 14:59:19 Runner exited with error: exit status 1
2025/09/27 14:59:19 exit status 1
FAIL fizzbee-mbt-quickstart/fizztests 0.257s
FAIL
You’ll see a trace something like the above. FizzBee tries various sequence of actions, until the first failure.
In this case, it tried two Inc
actions followed by a Get
action. So, the expected value is 2, but our Get
action always returns 0.
When you run, you might see a different trace. That is because, FizzBee explores the state space randomly. You can execute the same trace again by using the
--seq-seed
option with the seed value shown in the output.At present, FizzBee MBT does not implement shrinking. So, the trace might be longer than the minimal trace. You can find shorter traces by passing
--max-actions
option with a smaller value. For example,--max-actions=2
.go test ./fizztests --max-actions=2 Validation failed: Failed: Counter#0.Get, expected: [int_value:1], actual: [int_value:0] 0: Counter#0.Inc() ==> 1: Counter#0.Get() -> 0 Return value mismatched Expected: 1: Counter#0.Get() -> 1 Test failure after 20 trace(s). To retry the same trace, use: --seq-seed=1759010533300160000 2025/09/27 15:02:13 Runner exited with error: exit status 1 2025/09/27 15:02:13 exit status 1 FAIL fizzbee-mbt-quickstart/fizztests 0.270s FAIL
Let us try a minimal implementation to pass the tests. Just add a counter field to the adapter struct.
In most implementations, you probably will have a separate Counter struct that has the state and methods. The adapter will just call the methods on the struct. But for simplicity, we will keep everything in the adapter struct.
|
|
Then, fill in the methods.
|
|
Now, run the tests again.
go test ./fizztests
Validation failed: Failed: Counter#0.Get, expected: [int_value:3], actual: [int_value:5]
0: Counter#0.Inc()
1: Counter#0.Get() -> 1
2: Counter#0.Inc()
3: Counter#0.Inc()
4: Counter#0.Inc()
5: Counter#0.Inc()
==> 6: Counter#0.Get() -> 5
Return value mismatched
Expected:
6: Counter#0.Get() -> 3
Test failure after 22 trace(s).
To retry the same trace, use: --seq-seed=1759020910396258000
2025/09/27 17:55:10 Runner exited with error: exit status 1
2025/09/27 17:55:10 exit status 1
FAIL fizzbee-mbt-quickstart/fizztests 0.282s
FAIL
In our model, the counter has a maximum value of 3. So, the Inc
action should be a no-op after it reaches 3.
You might be able to reproduce the smallest trace above with --max-actions=5
option.
|
|
Now run the tests again.
go test ./fizztests
ok fizzbee-mbt-quickstart/fizztests 1.410s
The tests passed! Try running with -v
flag to see the number of runs.
=== RUN TestCounter
Sequential tests succeeded! Number of sequential runs: 1000
Parallel tests succeeded! Number of parallel runs: 0
2025/09/27 18:19:37 Test executor shut down gracefully.
--- PASS: TestCounter (1.22s)
PASS
ok fizzbee-mbt-quickstart/fizztests 1.438s
The tests passed. You have successfully implemented the adapter for the simple counter model. For the sequential tests, FizzBee tried 1000 different sequences of actions.
Now, let’s enable the parallel tests by setting max-parallel-runs
to a non-zero value.
|
|
Run it again.
go test ./fizztests
Sequential tests succeeded! Number of sequential runs: 1000
1: START Counter#0.Inc()
1: END
0: START Counter#0.Inc()
0: END
1: START Counter#0.Inc()
1: END
1: START Counter#0.Get()
1: END 2
1: START Counter#0.Get()
1: END 2
1: START Counter#0.Inc()
1: END
1: START Counter#0.Inc()
1: END
Validation failed: Validation failed
Wrote validate request to /tmp/fizzmo-validate-req-3170371744.json
Test failure after 2789 trace(s).
To retry the same trace, use: --parallel-seed=1759023986695434000
2025/09/27 18:46:26 Runner exited with error: exit status 1
2025/09/27 18:46:26 exit status 1
FAIL fizzbee-mbt-quickstart/fizztests 2.688s
FAIL
This is a random attempt. You might see a different trace. If it passes, you might have to run multiple times. But with Go, there is a better way to find concurrency bugs. Read the next section.
If it fails, you will see a trace like the above. The trace shows interleaved actions from two parallel runs. It is a bit hard to read. FizzBee uses porcupine for checking linearizability of concurrent operations. You can see the generated trace by opening the trace visualization at
open /tmp/fizzbee-porcupine.html
You might see something like,
You could see, the concurrency issue. An Inc() from another thread was ignored.
Go comes with a great tool called Data Race Detector. You can enable it by passing -race
flag to go test
.
go test -race ./fizztests
It will print a lot of stack trace. The interesting part is at the top.
==================
WARNING: DATA RACE
Write at 0x00c0005a1038 by goroutine 22044:
fizzbee-mbt-quickstart/fizztests.(*CounterRoleAdapter).ActionInc()
src/fizzbee-mbt-examples/examples/fizzbee-mbt-quickstart/fizztests/counter_adapters.go:22 +0x50
fizzbee-mbt-quickstart/fizztests.init.func1()
src/fizzbee-mbt-examples/examples/fizzbee-mbt-quickstart/fizztests/counter_test.go:18 +0x9c
github.com/fizzbee-io/fizzbee/mbt/lib/go.(*FizzBeeMbtPluginServer).executeCommand()
src/fizzbee/mbt/lib/go/plugin_service.go:320 +0xb4
github.com/fizzbee-io/fizzbee/mbt/lib/go.(*FizzBeeMbtPluginServer).executeSequence()
src/fizzbee/mbt/lib/go/plugin_service.go:207 +0x78
github.com/fizzbee-io/fizzbee/mbt/lib/go.(*FizzBeeMbtPluginServer).executeSequencesConcurrent.func1()
src/fizzbee/mbt/lib/go/plugin_service.go:230 +0x5c
golang.org/x/sync/errgroup.(*Group).Go.func1()
go/pkg/mod/golang.org/x/sync@v0.16.0/errgroup/errgroup.go:93 +0x7c
Previous read at 0x00c0005a1038 by goroutine 22043:
fizzbee-mbt-quickstart/fizztests.(*CounterRoleAdapter).ActionGet()
src/fizzbee-mbt-examples/examples/fizzbee-mbt-quickstart/fizztests/counter_adapters.go:28 +0x2c
fizzbee-mbt-quickstart/fizztests.init.func2()
src/fizzbee-mbt-examples/examples/fizzbee-mbt-quickstart/fizztests/counter_test.go:21 +0x9c
github.com/fizzbee-io/fizzbee/mbt/lib/go.(*FizzBeeMbtPluginServer).executeCommand()
src/fizzbee/mbt/lib/go/plugin_service.go:320 +0xb4
github.com/fizzbee-io/fizzbee/mbt/lib/go.(*FizzBeeMbtPluginServer).executeSequence()
src/fizzbee/mbt/lib/go/plugin_service.go:207 +0x78
github.com/fizzbee-io/fizzbee/mbt/lib/go.(*FizzBeeMbtPluginServer).executeSequencesConcurrent.func1()
src/fizzbee/mbt/lib/go/plugin_service.go:230 +0x5c
golang.org/x/sync/errgroup.(*Group).Go.func1()
go/pkg/mod/golang.org/x/sync@v0.16.0/errgroup/errgroup.go:93 +0x7c
Let’s look at the first location.
|
|
and the second location.
|
|
That is, both are accessing the count
field concurrently without synchronization.
Let’s fix it by adding a mutex to the adapter struct.
type CounterRoleAdapter struct {
mu sync.Mutex
count int
}
And use the mutex in the methods.
func (a *CounterRoleAdapter) ActionInc(args []mbt.Arg) (any, error) {
a.mu.Lock()
defer a.mu.Unlock()
if a.count < 3 {
a.count++
}
return nil, nil
}
func (a *CounterRoleAdapter) ActionGet(args []mbt.Arg) (any, error) {
a.mu.Lock()
defer a.mu.Unlock()
return a.count, nil
}
Now run the tests again.
go test -race ./fizztests
ok fizzbee-mbt-quickstart/fizztests 15.123s
In the model, there is a Dec action. But we haven’t implemented it yet. Implement a dummy implementation that does nothing and see the tests fail.
Implement the Dec action to pass the sequential tests.
Tip: You can temporarily disable without removing the parallel tests by passing--max-parallel-runs=0
flag.
Tip: You can disable sequential tests by passing --max-seq-runs=0
flag.
Implement the Dec action to pass the parallel tests.
So far, we kept everything in the adapter struct for simplicity. But in a real implementation, you will be testing a real Counter usually in a different package. Most likely, you will be testing an existing code. Either as a library, or as a service through its API - either REST API or gRPC. Also, the counter might be persisted in a database or a shared cache like redis.
For this example, let us refactor the code to use a separate Counter struct.
file: ./mem_counter.go
type MemCounter struct {
mu sync.Mutex
// limit is the maximum value of the counter
limit int
// value is the current value of the counter
value int
}
// NewMemCounter creates a new MemCounter with the given limit.
func NewMemCounter(limit int) *MemCounter {
return &MemCounter{limit: limit}
}
Implement the methods - Inc, Dec, and Get.
Now, modify the adapter to use the MemCounter struct.
file: ./fizztests/counter_adapters.go
Update the action methods to call the methods on the MemCounter struct.
You can check the code coverage of your tests by passing -coverprofile
flag to go test
.
go test -race -count=1 -coverprofile ./coverage.out -coverpkg=./ ./fizztests
ok fizzbee-mbt-quickstart/fizztests 327.110s coverage: 82.8% of statements in ./
Then generate the HTML report.
go tool cover -html=coverage.out
Congratulations! You have completed the FizzBee MBT quickstart tutorial. In this tutorial, you have learned how to create a model, implement the adapter, run sequential and parallel tests, and check code coverage. You can now apply these skills to test your own applications using FizzBee MBT.