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 Here4
  • UVM Foundations14
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
  • Environment & Test Classes11
    • Environment Architecture
    • Tests: Base Classes, Selection and Startup
      • Base Test Pattern: One Contract for Every Scenario
      • Scenario Inheritance: Specialize Intent, Not Infrastructure
      • Factory Overrides in Tests: Explicit, Local, Reviewable
      • Shared run_phase: One End-of-Test Policy for Every Test
      • +UVM_TESTNAME and Plusargs: Runtime Test Selection
      • Top Module and run_test Startup
      • Designing a Test Library
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Tests: Base Classes, Selection and Startup
  1. Topics
  2. UVM Verification
  3. Environment & Test Classes
  4. Tests: Base Classes, Selection and Startup
  5. Base Test Pattern: One Contract for Every Scenario

Part 7 · Environment & Tests · Intermediate

Base Test Pattern: One Contract for Every Scenario

What dma_base_test guarantees to every derived test (the env, the interrupt interface, the topology print and the reg_write/reg_read/run_job helpers), what it leaves out on purpose, and what a production base test adds.

On this page

    Open any test on the DMA bench and it is short: dma_rand_test is about twenty lines, dma_truncate_test about twenty-five. A colleague reviewing them suggests deleting dma_base_test and pasting its fifty lines into each test, "so every test is self-contained and easy to read on its own." Nine tests later, someone renames the APB agent. How many files change, and how many of them will be missed?

    This lesson takes dma_base_test apart: what it promises every derived test, why each line is there, what it deliberately does not do, and what a base test on a real project adds on top.

    The idea: a base test is a contract, not a convenience

    A base test is the class every scenario test extends. Its job is to make a promise: after super.build_phase() returns, the environment exists, the handles every scenario needs are in hand, and a small vocabulary of operations is ready to use. A derived test then only says what is different about its scenario. That promise is the contract, and its value is that it is written once.

    Think of a film studio's standing set. The street, the lighting rig and the camera tracks are built once; each scene is shot on them by changing the actors and the script. Nobody rebuilds the street for every scene, and if the street needs a new lamp post, it goes up once and every later scene has it. The base test is the set; each derived test is a scene.

    The picture

    Diagram: dma_base_test contains build_phase, end_of_elaboration and helpers; build creates dma_env and gets irq_vif; helpers use the env's sequencers and the interrupt; derived tests call the helpers.
    What dma_base_test owns. Build creates the env and fetches the interrupt interface; end of elaboration prints the topology; the helpers drive the env's sequencers and wait on the interrupt. Derived tests mostly write a run_phase that calls the helpers.

    Build it: dma_base_test, line by line

    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
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    // snippet: from the course DMA bench
    class dma_base_test extends uvm_test;
      `uvm_component_utils(dma_base_test)
      dma_env env;
      virtual irq_if irq_vif;
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        env = dma_env::type_id::create("env", this);
        if (!uvm_config_db#(virtual irq_if)::get(this, "", "irq_vif", irq_vif))
          `uvm_fatal("NOVIF", "irq_if not set for the test")
      endfunction
    
      function void end_of_elaboration_phase(uvm_phase phase);
        uvm_root::get().print_topology();
      endfunction
    
      // Register helpers used by every test.
      task reg_write(bit [7:0] addr, bit [31:0] data);
        apb_rw_seq s = apb_rw_seq::type_id::create("s");
        s.addr = addr; s.write = 1; s.data = data;
        s.start(env.apb.sqr);
      endtask
    
      task reg_read(bit [7:0] addr, output bit [31:0] data);
        apb_rw_seq s = apb_rw_seq::type_id::create("s");
        s.addr = addr; s.write = 0; s.data = 0;
        s.start(env.apb.sqr);
        data = s.rdata;
      endtask
    
      // One complete job: program the DMA, stream n words, wait for the interrupt,
      // return STATUS and clear its DONE/ERR bits.
      task run_job(dma_job_cfg job, int unsigned n_words, output bit [31:0] status);
        axis_pkt_seq pkt = axis_pkt_seq::type_id::create("pkt");
        reg_write(KEY, job.key);
        reg_write(LEN, job.len);
        reg_write(CTRL, {30'b0, job.irq_en, 1'b1});
        if (!pkt.randomize() with { n == n_words; })
          `uvm_fatal("RAND", "axis_pkt_seq randomize failed")
        pkt.start(env.in_agt.sqr);
        wait (irq_vif.irq === 1'b1);
        reg_read(STATUS, status);
        reg_write(STATUS, 32'h6);                // write 1 to clear DONE and ERR
      endtask
    endclass

    build_phase: the env through the factory, the interface or a fatal

    super.build_phase(phase) comes first. It is what applies configuration to the test's own fields (fact: uvm_component::build_phase calls apply_config_settings() when automatic configuration is on), and skipping it is a habit that bites later, in classes that do have such fields. Then the env is created with type_id::create, never new, so that a derived test's factory override can replace the env or anything inside it. Factory Overrides in Tests shows four tests that rely on it.

    The interrupt interface is fetched with a fatal on failure. A test that forgets to check would continue with a null handle and die later inside wait (irq_vif.irq === 1'b1), far from the cause. The fatal at time 0 says exactly which handle is missing. Every derived test inherits that check without writing it.

    end_of_elaboration_phase: print what was really built

    print_topology() lists every component with its actual type. After a factory override that is the only proof that the override hit: in dma_backpressure_test the table shows out_agt as axis_sink_agent while in_agt stays axis_agent. It runs in end of elaboration because by then every component exists and every connection is made, but no time has passed.

    The helpers: a vocabulary, not a scenario

    • reg_write and reg_read each create an apb_rw_seq and start it on env.apb.sqr. The read data comes back in the sequence's rdata field: the driver writes it into the request item and calls item_done() with no argument, so there is no response queue to drain. The driver handshake explains why.

    • run_job is one complete DMA job: program KEY and LEN, start with GO (and IRQ_EN from the job), stream n_words words, wait for the interrupt, read STATUS and clear DONE and ERR. Its n_words argument is separate from job.len on purpose, so a test can send fewer words than it programmed.

    • None of them raises an objection. They run inside a derived test's run_phase, which owns the objection.

    What the base test leaves out, on purpose

    There is no run_phase in dma_base_test, so no stimulus and no objection: every derived test decides what to run and when it is done. There is no timeout either; among the tests in this part only dma_timeout_test sets one (the debug playbooks' watchdog and hang tests set their own later). Both are choices worth arguing about, and Shared run_phase argues them. There is also no configuration object, because the block env hard-wires its topology. The Environment and Its Config Object adds one.

    What a production base test adds

    On a real project the base test grows a few more duties. The listing below shows them on the DMA bench: an env config object built in a fixed order (class defaults, then the scenario, then the command line, then a validity check), a global timeout, and a verdict line. The order matters: the command line comes after the scenario so that you can change a test's knob for one debug run without editing the test.

    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
    // snippet
    class dma_proj_base_test extends uvm_test;
      `uvm_component_utils(dma_proj_base_test)
      dma_env        env;
      dma_env_cfg    cfg;                 // topology knobs, see the env-architecture topic
      virtual irq_if irq_vif;
      int unsigned   n_jobs = 3;          // a scenario knob with its default
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      // Hooks for derived tests, called in this order by build_phase.
      virtual function void configure_scenario(); endfunction   // edit cfg and knobs
      virtual function void apply_overrides();    endfunction   // factory overrides
    
      function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        cfg = dma_env_cfg::type_id::create("cfg");                // 1. class defaults
        configure_scenario();                                     // 2. the test's intent
        void'($value$plusargs("DMA_JOBS=%d", n_jobs));            // 3. the command line wins
        if (n_jobs == 0) `uvm_fatal("CFG", "DMA_JOBS must be at least 1")   // 4. validate
        apply_overrides();
        uvm_config_db#(dma_env_cfg)::set(this, "env", "cfg", cfg);
        env = dma_env::type_id::create("env", this);
        if (!uvm_config_db#(virtual irq_if)::get(this, "", "irq_vif", irq_vif))
          `uvm_fatal("NOVIF", "irq_if not set for the test")
        uvm_root::get().set_timeout(2ms, 1);                       // no test may hang a farm slot
      endfunction
    
      function void report_phase(uvm_phase phase);
        uvm_report_server rs = uvm_report_server::get_server();
        int bad = rs.get_severity_count(UVM_ERROR) + rs.get_severity_count(UVM_FATAL);
        `uvm_info("VERDICT", bad == 0 ? "TEST PASSED" : "TEST FAILED", UVM_NONE)
      endfunction
    endclass

    Two cautions come with it. The verdict line never appears after a fatal, because report_phase does not run then; regression scripts must judge by the report summary's counts, not by the banner. And the timeout set with overridable 1 replaces a +UVM_TIMEOUT=...,YES from the command line; run_test and timeouts explains the order.

    Watch it run

    Every test prints the same topology header, because the base test prints it. Here is the start of the smoke test's run, port rows trimmed:

    Diagram
    UVM_INFO @ 0: reporter [RNTST] Running test dma_smoke_test...
    UVM_INFO uvm_root.svh(531) @ 0: reporter [UVMTOP] UVM testbench topology:
    Name                         Type                    Size  Value
    <unnamed>                    uvm_root                -     @70
      uvm_test_top               dma_smoke_test          -     @180
        env                      dma_env                 -     @250
          apb                    apb_agent               -     @264
            drv                  apb_driver              -     @466
            mon                  apb_monitor             -     @326
            sqr                  uvm_sequencer           -     @343
    ...
    UVM_INFO dma_tb.sv(411) @ 655000: uvm_test_top.env.sb [SB] 8 words matched, 0 mismatched
    UVM_ERROR :    0
    UVM_FATAL :    0
    • uvm_test_top has the derived type, dma_smoke_test, but the env row under it was built by the base test's build_phase.

    • The @ numbers are object ids. They differ between simulators and runs; never compare them.

    What breaks: the override that deleted the environment

    An engineer adds a test that needs a different driver. They copy the shape of dma_truncate_test, write a build_phase that registers the override, and in the rush leave out super.build_phase(phase). It compiles. At time 0 the run dies with a null-object access inside reg_write, on the line that starts the sequence on env.apb.sqr.

    The topology print gives it away: it shows uvm_test_top and nothing under it. The derived build_phase replaced the base one entirely, so the env was never created and irq_vif was never fetched. The fix is one line, and the habit that prevents it is simple: every build_phase override starts or ends with super.build_phase(phase), and reviewers look for it. The same mistake on a component with field macros is quieter: nothing crashes, but configuration from config_db is never applied.

    Interview

    "What goes in your base test, and what stays out?"

    A weak answer: "Common stuff: creating the env and some utility functions." It names no decision.

    A strong answer: "The base test is a contract: after super.build_phase the env exists, built through the factory so derived tests can override anything; required handles are fetched with a fatal on failure; the topology is printed at end of elaboration so overrides are visible. It gives a small vocabulary such as reg_write and run_job. It owns policy every test shares: the env config defaults, the order defaults, scenario, command line, validate, and a global timeout. It stays out of scenario intent: which stimulus, which overrides and what to check belong to the derived test."

    Follow-ups: Why is the command line applied after the scenario? (so one debug run can change a test's knob without editing it). Should run_job live in the base test or in a sequence? (in a sequence once a second test class needs it, see Designing a Test Library).

    Check yourself A derived test overrides build_phase, calls super.build_phase(phase) first, and then sets uvm_config_db#(int)::set(this, "env.sb", "verbose", 1). Does the scoreboard see it?

    Yes. super.build_phase creates the env object, but the env's own build_phase, which creates the scoreboard, runs only after the test's build_phase returns (build is top-down). By the time the scoreboard's build_phase calls get, the set is already there. The same reasoning is why many overrides still work when registered after super.build_phase, as the overrides lesson shows.

    Check yourself Why does dma_base_test fetch irq_vif with a fatal, when wait (irq_vif.irq === 1'b1) would fail anyway if the handle were null?

    Because of where and when it fails. The fatal fires at time 0 in build with a message naming the missing handle. The null access would fire later, inside run_job, possibly after minutes of simulation, with a message about a null object that sends you looking at the sequence or the DUT.

    Check yourself A teammate wants the base test to raise the run-phase objection so derived tests cannot forget. What breaks with the bench as it is?

    Every derived test already raises and drops its own objection, so nothing breaks immediately, but the base test's objection would have to be dropped by someone. If the base drops it at the end of an empty run_phase, it adds nothing; if it waits for a hook, the derived tests must be rewritten to use the hook instead of run_phase. The fix is a template-method base, which is a design change, not a one-line addition.

    Next: the base test is the set; now read the nine scenes. Scenario Inheritance sorts the derived tests by what each one changes.

    Related topics

    • Scenario Inheritance

    • What super.build_phase does

    • The Environment and Its Config Object

    • Your first UVM testbench

    Previous

    119 / 280

    Next: Scenario Inheritance: Specialize Intent, Not Infrastructure

    Up next

    Up nextScenario Inheritance: Specialize Intent, Not Infrastructure

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Factory Overrides in Tests: Explicit, Local, Reviewable
    • Shared run_phase: One End-of-Test Policy for Every Test
    • +UVM_TESTNAME and Plusargs: Runtime Test Selection