Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here6
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
  • Constraint Randomization — Interview-Critical Deep Dive36
  • SystemVerilog Assertions — Interview-Critical Deep Dive40
  • Functional Coverage in SystemVerilog22
  • Testbench Architecture in SystemVerilog23
    • Testbench Anatomy & Layering
      • The Layered Testbench
      • The Static Harness and Environment
      • Tests as Classes
      • Complete Mini Testbench
    • Stimulus: Generators, Transactions & Drivers
    • Monitors, Scoreboards & Reference Models
    • Self-Checking Tests & Regression
    • From Hand-Built TB to UVM
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
Loading lesson

‹Testbench Anatomy & Layering
  1. Topics
  2. SystemVerilog
  3. Testbench Architecture in SystemVerilog
  4. Testbench Anatomy & Layering
  5. Tests as Classes

Part 6 · Testbench Architecture · Intermediate

Tests as Classes

Base and derived test classes, plusarg test selection, and the contract for ending a test.

On this page

    The base test

    The test layer answers one question: what runs today, and with which knobs. A base test owns the environment handle, sets sane defaults, and defines the run skeleton that every derived test reuses.

    The test class listing below is a fragment, so this course's checks do not run it. It builds the environment from the harness lesson, extended with a report and an error-rate knob.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // not-run: fragment: needs the env class and bus_if interface from the harness listing, and has no top module
    class base_test;
      env            e;
      virtual bus_if vif;          // assigned by tb_top
    
      // Default knobs: derived tests override them 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.build();
        e.set_err_pct(err_pct);    // defined on env, forwarded to gen
        e.run();                   // blocks until env done criteria
        e.report();
      endtask
    endclass

    The error-rate call goes to a real method. The environment defines set_err_pct and forwards it to the generator, so the call compiles and the knob has one owner.

    Derived tests

    Each derived test is a few lines long. That brevity proves the layering worked: all protocol and plumbing knowledge lives below the test layer.

    The derived tests below are not run either, for the same reason: they extend the base test fragment above.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    // not-run: fragment: extends base_test from the listing above, and has no top module
    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
    endclass
    
    class error_test extends base_test;
      virtual function void configure();
        num_txns = 1000;
        err_pct  = 15;             // 15 percent of txns stress error paths
      endfunction
    endclass
    
    // Constraint-narrowed flavor: aim stimulus at one address window.
    // Generator knobs are written between build() and run(), keeping
    // every plumbing line of the base run. No undeclared blueprint.
    class small_addr_test extends base_test;
      virtual function void configure();
        num_txns = 500;
      endfunction
      virtual task run();
        e = new();
        configure();
        e.vif = vif;
        e.num_txns = num_txns;
        e.build();
        e.gen.addr_lo = 16'h0000;
        e.gen.addr_hi = 16'h00FF;
        e.set_err_pct(err_pct);
        e.run();
        e.report();
      endtask
    endclass

    Test

    Knobs

    Question it answers

    smoke_test

    20 transactions, defaults

    Is anything alive at all.

    stress_test

    50000 transactions

    What breaks under volume and backpressure.

    error_test

    1000 transactions, 15 percent error rate

    Do the DUT error paths behave.

    small_addr_test

    500 transactions, low address window

    Does one feature corner hold up.

    Check yourself A derived test overrides run() and skips the base plumbing. What breaks?

    The environment is half built: handles stay null and wiring never happens. Override configure() for knobs, and override run() only to add knob writes between build() and run() while keeping every plumbing line.

    Selecting tests without recompiling

    Without the UVM factory, the workable pattern is a plusarg string with a case ladder in tb_top. The base-class handle holds any derived test, and the virtual run() dispatches to it.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    // snippet
    // Factory-lite: map a plusarg string to an object. The ladder is
    // partial here: clock, reset, interface and DUT live in the
    // harness listing.
    module tb_top_sel;
      // ... clock, reset, interface bif, 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";
    
        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("TB_TOP: unknown TESTNAME, stopping");
            $finish;
          end
        endcase
    
        t.vif = bif;
        wait (rst_n === 1);
        t.run();              // virtual: dispatches to the derived test
        $finish;
      end
    endmodule
    
    // Run: <simulator> +TESTNAME=stress_test
    • End of test needs both sides: stimulus exhausted AND the scoreboard drained, never just one.

    • A drain window of idle cycles after the last stimulus lets in-flight responses emerge.

    • The watchdog stays: a test that never meets its done criteria must die loudly, not hang the farm.

    Interview

    How do you select tests without recompiling, and what is wrong with the case statement.

    • Plusarg plus ladder picks the test at run time with no recompile.

    • The ladder centralizes knowledge of every test in tb_top, so adding a test edits shared code.

    • That cost motivates the UVM factory, where self-registering proxies remove the ladder.

    Common pitfall

    Ending the test when the generator finishes leaves in-flight responses unchecked. Putting knob values in tb_top instead of test classes stops tests being self-describing, and a missing default case turns a TESTNAME typo into a silent no-op.

    Testbench

    A runnable version of the same ideas: a numeric plusarg selects smoke, stress, or targeted stimulus, and the test ends only after stimulus, drain, and a full read-back.

    sel_dut.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    `timescale 1ns/1ps
    module sel_dut (
      input  logic        clk,
      input  logic        rst_n,
      input  logic        valid,
      input  logic        write,
      input  logic [3:0]  addr,
      input  logic [31:0] wdata,
      output logic        ready,
      output logic [31:0] rdata
    );
      logic [31:0] mem [0:15];
      assign ready = 1'b1;
      always @(posedge clk or negedge rst_n) begin
        if (!rst_n) rdata <= 32'h0;
        else if (valid && write) begin
          mem[addr] <= wdata;
          rdata <= 32'h0;
        end
        else if (valid && !write) rdata <= mem[addr];
      end
    endmodule
    
    Check yourself Why does the runnable test use TESTID=%d while the reference ladder uses TESTNAME=%s?

    The string case in the ladder crashes Icarus 13; Verilator 5.048 runs it. The numeric select keeps the same idea, one handle serving many tests, in a block both tools run. The string ladder stays the pattern to copy for real flows.

    Related topics

    • Self-checking regression

    Previous

    165 / 259

    Next: Complete Mini Testbench

    Up next

    Up nextComplete Mini Testbench

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • The Layered Testbench