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.

systemverilog
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
endclass

Derived 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.

systemverilog
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
endclass

Test 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.

systemverilog
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=7

End-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.