UVM Test Explained: Building the Env and Objections

A uvm test is the top-level component of a UVM testbench and the single object you hand to the simulator when you launch a run. Extending uvm_test gives your class the phase hooks, factory registration, configuration database, and reporting machinery that everything else depends on. In practice a test does exactly three jobs: it builds and configures the environment, it decides which stimulus runs, and it keeps the simulation alive for precisely as long as that stimulus needs.

The UVM examples on this page are UVM-library code and need a UVM-capable simulator (industry-standard simulation tools, VCS, or industry-standard simulators) or a free EDA Playground session to elaborate and run. They are shown as syntax references, not as runnable snippets. The one plain-SystemVerilog illustration near the end runs on any simulator with no UVM library at all.

Extending uvm_test

Every test starts by extending uvm_test and registering with the factory using `uvm_component_utils. Registration is what lets you swap tests from the command line later. A sensible pattern is to write one base test that builds the environment, then derive small concrete tests from it that only change the stimulus.

class base_test extends uvm_test;
  `uvm_component_utils(base_test)

  my_env     m_env;   // the environment this test drives
  env_config m_cfg;   // configuration object for that env

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
endclass

Note the new signature: a uvm_component always takes a name and a parent. The test itself is created by the UVM core with a null parent, so you never call its constructor by hand.

Building the environment in build_phase

The environment is constructed inside build_phase, which UVM calls top-down. Always call super.build_phase(phase) first, create any configuration objects, publish them to the uvm_config_db so lower components can read them, and then create the environment through the factory with type_id::create. Using the factory (not plain new) is what makes components overridable.

function void base_test::build_phase(uvm_phase phase);
  super.build_phase(phase);

  // 1. Create and fill a config object
  m_cfg = env_config::type_id::create("m_cfg");
  m_cfg.num_agents = 2;
  m_cfg.is_active  = UVM_ACTIVE;

  // 2. Hand it to the env and everything below it
  uvm_config_db#(env_config)::set(this, "m_env*", "cfg", m_cfg);

  // 3. Build the environment through the factory
  m_env = my_env::type_id::create("m_env", this);
endfunction

Because build_phase runs top-down, the test is built before its environment, and the environment is built before its agents. That ordering is exactly why the config object must be set before the child is created: the child reads it in its own build_phase, which happens afterwards.

Selecting and starting the top sequence in run_phase

Stimulus is launched in run_phase, the one phase that consumes simulation time. A concrete test selects a sequence, creates it from the factory, and calls start() on the target sequencer. The sequencer handle usually lives deep in the environment, so the test reaches it through the component hierarchy.

class smoke_test extends base_test;
  `uvm_component_utils(smoke_test)

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  task run_phase(uvm_phase phase);
    traffic_seq seq;
    seq = traffic_seq::type_id::create("seq");

    phase.raise_objection(this, "smoke_test: stimulus starting");
    seq.start(m_env.m_agent.m_sequencer);   // blocks until the sequence finishes
    phase.drop_objection(this, "smoke_test: stimulus complete");
  endtask
endclass

Because smoke_test extends base_test, the environment is already built for it. The derived test only has to pick a sequence and run it. To create a second scenario you write another short class that starts a different sequence, reusing the same base.

Raising and dropping objections

UVM does not know when your stimulus is done, so you tell it with objections. Calling phase.raise_objection(this) before you start the sequence signals “the run phase is not allowed to end yet.” Calling phase.drop_objection(this) after start() returns signals that this test is finished. The run phase ends only once the total objection count across the whole testbench reaches zero.

If you forget to raise an objection, run_phase ends immediately and your sequence may never drive a single transaction. If you forget to drop it, the simulation hangs until the global timeout fires. For stimulus that trails off gradually you can also set a drain time with phase.phase_done.set_drain_time(this, 100ns), which delays the end of the phase after the last objection drops.

ElementWhat it does
build_phaseConstructs config objects and the environment (top-down)
connect_phaseWires analysis ports and TLM connections (bottom-up)
run_phaseThe only time-consuming phase; where sequences start
raise_objectionKeeps the run phase alive while stimulus is active
drop_objectionReleases the phase so the simulation can end cleanly
report_phasePrints the pass/fail summary after the run completes

run_test() and +UVM_TESTNAME

A UVM simulation is kicked off by a single call to run_test() inside the top module’s initial block. This call creates the test component, runs the full phase schedule, and returns when everything is done.

module top;
  // clock, reset, interface and DUT instantiation go here

  initial begin
    run_test();   // test name supplied from the command line
  end
endmodule

Leaving run_test() with no argument is the idiomatic choice because it lets you pick the test at run time through the +UVM_TESTNAME plusarg, with no recompile:

# run the same compiled image with different tests
simv +UVM_TESTNAME=smoke_test
simv +UVM_TESTNAME=stress_test
simv +UVM_TESTNAME=error_inject_test

The name you pass must match a factory-registered class, which is why `uvm_component_utils matters. You can also hard-code a default with run_test("smoke_test"), but +UVM_TESTNAME always wins over that argument, so a command-line name overrides whatever is compiled in. One image, many tests, chosen at launch.

The objection idea, in plain SystemVerilog

You cannot run the UVM blocks above without a UVM-capable simulator, but the mechanism behind objections is easy to model with nothing but plain SystemVerilog. The example below uses a simple pending counter: a stand-in “sequence” increments it before driving and decrements it when done, and the “run phase” ends only when the count returns to zero. Run it on any simulator to watch the ordering.

module tb;
  int objections = 0;

  task raise();
    objections++;
    $display("[%0t] raise -> pending=%0d", $time, objections);
  endtask

  task drop();
    objections--;
    $display("[%0t] drop  -> pending=%0d", $time, objections);
  endtask

  // A stand-in "sequence": raise before driving, drop when done.
  task automatic run_sequence();
    raise();
    for (int i = 0; i < 3; i++) begin
      #10 $display("[%0t] drive transaction %0d", $time, i);
    end
    drop();
  endtask

  initial begin
    $display("[%0t] run_phase begins", $time);
    run_sequence();
    // The "test" stays alive until every objection is dropped.
    wait (objections == 0);
    $display("[%0t] all objections dropped; ending run", $time);
    $finish;
  end
endmodule

[0] run_phase begins
[0] raise -> pending=1
[10] drive transaction 0
[20] drive transaction 1
[30] drive transaction 2
[30] drop  -> pending=0
[30] all objections dropped; ending run
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp_js1p_2p/tb.sv:29: $finish called at 30 (1s)

Swap the counter for phase.raise_objection/drop_objection, the manual loop for a real sequence on a sequencer, and the initial block for run_phase, and you have the UVM version. The control flow is identical.

A reusable base-test pattern

Putting it together, a maintainable test hierarchy looks like this:

  • base_test builds the environment and sets configuration in build_phase; it starts nothing.
  • concrete tests extend the base and override only run_phase to start their own sequence.
  • Every test raises an objection before stimulus and drops it after, so the run ends on its own.
  • The top module calls run_test() with no argument, and +UVM_TESTNAME selects which one runs.

With that structure, adding a new scenario is a dozen lines, and your whole regression runs off one compiled image driven entirely by the command line.

Quick check

Why must you register a test with `uvm_component_utils?

Registration adds the test to the UVM factory, which is what lets +UVM_TESTNAME (or run_test("name")) look the class up by string and construct it at run time. Without it, the name cannot be resolved.

What happens if run_phase has no raise_objection?

The run phase has nothing holding it open, so it ends immediately. Your sequence may never drive any transactions, and the simulation finishes almost at time zero.

Where is the top sequence actually started, and on what?

Inside run_phase, by calling seq.start(sequencer) on the target sequencer handle reached through the environment hierarchy. start() blocks until the sequence completes.

If you compile with run_test(“smoke_test”) but launch with +UVM_TESTNAME=stress_test, which runs?

stress_test. The +UVM_TESTNAME plusarg overrides the string argument passed to run_test(), so the command line always wins.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.
▶ Try it in the ChipXpert Lab

Compile and simulate this in the browser — no install. Edit the code and re-run instantly.

Open in Verification Lab

Learn Verilog with mentors, labs and placement support

This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.

Explore VLSI Courses
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD