Start with observable behavior
For a small RTL block, define an input transaction, expected output and permitted latency. Ask AI to derive a test matrix from the specification: reset, normal transfers, boundary values and stalls. Providing only the implementation and asking for tests risks reproducing its mistakes. Write down an independent reference model before building stimulus around it.
Sample at the right time
A clock edge and settled outputs are different points in the simulation model. To sample final values for the current time step, wait for ReadOnly after the edge as shown below. This fragment belongs inside an async test with clock dut.clk. Do not write signals in the same read-only phase; schedule drivers separately from monitors.
from cocotb.triggers import RisingEdge, ReadOnly
await RisingEdge(dut.clk)
await ReadOnly()
# Sample settled outputs here.Make failure explicit
Compare outputs with the reference using assertions, bound waits with timeouts and count accepted transactions. A log message alone does not fail a test. Check that the monitor actually observed traffic instead of completing without observations. For randomized stimulus, retain the seed, input sequence and first mismatching transaction so another engineer can reproduce the failure.
Retain a regression case
Record cocotb and simulator versions, HDL sources, top module and parameters. After a fix, rerun the original failure and nearby boundary cases. Ask AI to minimize a failing test while preserving its failure condition. Deliver a reproducible report and self-checking test. Also check RTL with lint and synthesis: simulation only exercises the scenarios you actually run.
Checks before accepting the result
A useful test detects the error, explains the mismatch and repeats with the same inputs.