Part 6 · Testbench Architecture · Intermediate
Tests as Classes
base_test with env handle and defaults, derived tests tweaking knobs and constraints, plusarg test selection, and end-of-test orchestration.
base_test: the template
The test layer answers one question: what scenario runs today, with which knobs? A base_test owns the env handle, sets sane defaults, and defines the run skeleton. Derived tests override only what differs — transaction counts, constraint weights, error injection — and contain zero plumbing.
class base_test;
env e;
virtual bus_if vif; // assigned by tb_top
// default knobs — derived tests override in configure()
int unsigned num_txns = 200;
int unsigned err_pct = 0;
virtual function void configure();
// base test keeps defaults
endfunction
virtual task run();
e = new();
configure(); // derived-class hook
e.vif = vif;
e.num_txns = num_txns;
e.gen_err_pct(err_pct); // pushed into generator constraints
e.build();
e.run(); // blocks until env done criteria
e.report();
endtask
endclassDerived tests: smoke, stress, error
Each derived test is a few lines. That brevity is the proof the layering worked — all protocol and plumbing knowledge lives below.
class smoke_test extends base_test;
virtual function void configure();
num_txns = 20; // fast bring-up: is anything alive?
endfunction
endclass
class stress_test extends base_test;
virtual function void configure();
num_txns = 50000; // volume: hit corner timing windows
endfunction
virtual task run();
super.run();
endtask
endclass
class error_test extends base_test;
virtual function void configure();
num_txns = 1000;
err_pct = 15; // 15% of txns randomized as errors
endfunction
endclass
// Constraint-override flavor: tighten stimulus from the test layer
class small_addr_test extends base_test;
virtual task run();
e = new();
configure();
e.vif = vif;
e.build();
// replace the generator's blueprint with a constrained subclass
begin
small_addr_txn blueprint = new();
e.gen.blueprint = blueprint; // factory-by-hand, see Stimulus topic
end
e.run();
e.report();
endtask
endclassTest taxonomy
Smoke test — tens of transactions, default constraints; gates every regression commit.
Stress test — maximal volume and rate; finds FIFO-depth and backpressure bugs.
Error test — protocol violations and error responses enabled; checks DUT error paths.
Targeted test — constraint-narrowed stimulus aimed at one feature or coverage hole.
Test selection: plusarg plus factory-lite
Without UVM's factory, the simplest workable pattern is a $value$plusargs string and an if/else ladder in tb_top. Crude, but transparent — and explaining its limits (every new test edits tb_top) is exactly how to motivate the UVM factory later.
module tb_top;
// ... clock, reset, interface, DUT as before ...
base_test t; // base-class handle holds any derived test
string testname;
initial begin
if (!$value$plusargs("TESTNAME=%s", testname))
testname = "smoke_test";
// factory-lite: map string → object (polymorphism does the rest)
case (testname)
"smoke_test": begin smoke_test st = new(); t = st; end
"stress_test": begin stress_test st = new(); t = st; end
"error_test": begin error_test et = new(); t = et; end
"small_addr_test": begin small_addr_test sa = new(); t = sa; end
default: begin
$display("FATAL: unknown +TESTNAME=%s", testname);
$finish;
end
endcase
t.vif = bif;
wait (rst_n === 1);
t.run(); // virtual — dispatches to the derived test
$finish;
end
endmodule
// Run: simv +TESTNAME=stress_test +ntb_random_seed=7End-of-test orchestration
The test ends when stimulus is exhausted AND the scoreboard has matched every expected transaction — both, never just one.
A drain time (a few hundred idle cycles) after the last stimulus lets in-flight DUT responses emerge before checks close.
The test calls e.report() last; the scoreboard prints PASS only if compare count matches and error count is zero.
The tb_top watchdog remains the backstop — a test that never meets its done criteria must die loudly, not hang the farm.
Interview angle
“How do you select tests without recompiling?” is the lead-in; the follow-up is “what is wrong with the case statement?” Answer: it centralizes knowledge of every test in tb_top, so adding a test means editing shared code — which is precisely the problem UVM's factory registration and +UVM_TESTNAME solve with self-registering proxies.
Key takeaways
base_test owns the env and defaults; derived tests override configure() with a handful of lines.
Polymorphism via a base-class handle plus virtual run() is what makes one tb_top serve many tests.
Plusarg + case ladder = factory-lite: workable, transparent, and the motivation for UVM's real factory.
End of test requires stimulus done AND scoreboard drained AND a drain window — then report.
Common pitfalls
Putting knob values in tb_top instead of test classes — tests stop being self-describing.
Ending the test when the generator finishes — in-flight responses go unchecked.
Forgetting the default case in the test selector — typo in +TESTNAME silently runs nothing.
Derived tests overriding run() and skipping super.run() plumbing — half-built env, null handles.