Verilog Testbench: Structure, Examples, Self-Checking

A Verilog testbench is a module that wraps around your design, drives its inputs, and watches its outputs. It is never synthesized into hardware, so it can use constructs the synthesis tool would reject: delays (#10), initial blocks, system tasks like $display, loops that run “forever”, and random numbers. In an interview or on the job, “can you write a testbench for this?” is often the very next question after “can you write the RTL?” — so treat this skill as equal in importance to design itself.

Structure of a Verilog testbench

Every testbench, from a 10-line mux check to a full verification environment, has the same skeleton. The testbench module has no ports — nothing drives it from outside; it is the top of the simulation.

module design_tb;            // no ports: this is the simulation top

  // 1. Signal declarations
  //    reg  for everything the testbench drives (DUT inputs)
  //    wire for everything the DUT drives (DUT outputs)

  // 2. DUT instantiation

  // 3. Clock generation (for sequential designs)

  // 4. Reset sequence + stimulus in initial blocks

  // 5. Observation: $monitor / $display, waveform dump, checkers

  // 6. $finish to end the simulation

endmodule

Two rules explain the reg/wire split, and they trip up almost every beginner:

  • Signals assigned inside an initial or always block (procedural assignment) must be declared reg. DUT inputs get their values this way, so they are reg in the testbench.
  • Signals driven by a module output port or a assign statement must be wire. DUT outputs are therefore wire in the testbench.

DUT instantiation

The design under test (DUT) is instantiated inside the testbench exactly like any submodule. Always use named port connections (.port_name(signal)) rather than positional ones. Positional connections silently break the day someone reorders the port list; named connections either stay correct or fail loudly at compile time.

A first complete testbench

Here is a 2-to-1 multiplexer and its testbench in one file. The initial block applies a new input pattern every 10 time units, and $monitor prints a line automatically whenever any signal in its list changes value.

// Design under test: 2-to-1 multiplexer
module mux2 (
  input  wire a,
  input  wire b,
  input  wire sel,
  output wire y
);
  assign y = sel ? b : a;
endmodule

// Testbench
module mux2_tb;
  reg  a, b, sel;   // testbench drives these, so they are reg
  wire y;           // DUT drives this, so it is wire

  // Instantiate the DUT using named port connections
  mux2 dut (
    .a   (a),
    .b   (b),
    .sel (sel),
    .y   (y)
  );

  initial begin
    // Print a line automatically whenever any listed signal changes
    $monitor("time=%0t  a=%b b=%b sel=%b -> y=%b", $time, a, b, sel, y);

    a = 0; b = 0; sel = 0;      // apply first input pattern
    #10 a = 1;                  // after 10 time units, change a
    #10 sel = 1;                // now y should follow b
    #10 b = 1;
    #10 sel = 0;                // back to a
    #10 $finish;                // end the simulation
  end
endmodule
time=0  a=0 b=0 sel=0 -> y=0
time=10  a=1 b=0 sel=0 -> y=1
time=20  a=1 b=0 sel=1 -> y=0
time=30  a=1 b=1 sel=1 -> y=1
time=40  a=1 b=1 sel=0 -> y=1
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpm49xkj0v/mux2_tb.v:33: $finish called at 50 (1s)

Note the difference between the two printing tasks you will use constantly:

System taskWhen it printsTypical use
$displayOnce, at the moment the statement executesStep-by-step messages, pass/fail reports
$monitorAutomatically on every change of any listed signal (only one monitor can be active)Compact trace of the whole run
$timeNot a print task — returns current simulation timeTimestamping messages; %0t formats it without padding

Without $finish, a simulation containing a free-running clock never ends — the simulator just keeps toggling the clock forever. Always terminate explicitly.

Clock and reset generation

Sequential designs need a clock. The standard idiom is one initial block to set the starting value and one always block to toggle it:

initial clk = 1'b0;    // define the starting value
always  #5 clk = ~clk; // toggle every 5 ns => 10 ns period = 100 MHz

If you skip the initial line, clk starts as x (unknown), and ~x is still x — the clock never toggles and your waveform is a flat red line. This single mistake accounts for a remarkable number of “my simulation does nothing” questions.

For reset, assert it at time 0, hold it for a couple of clock edges, then release it away from an active clock edge so you don’t create a race between reset release and the first capturing edge.

Dumping waveforms with $dumpfile and $dumpvars

$dumpfile("name.vcd") names a Value Change Dump file, and $dumpvars(0, tb_name) records every signal below the named scope (the first argument 0 means “all levels of hierarchy”). You can then open the .vcd file in GTKWave or any waveform viewer. The following example puts everything together: clock generation, an active-low asynchronous reset, an enable, and a waveform dump for a 4-bit counter.

// Design under test: 4-bit up counter with synchronous enable
module counter4 (
  input  wire       clk,
  input  wire       rst_n,   // active-low asynchronous reset
  input  wire       en,
  output reg  [3:0] count
);
  always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
      count <= 4'd0;
    else if (en)
      count <= count + 4'd1;
  end
endmodule

// Testbench
module counter4_tb;
  reg        clk, rst_n, en;
  wire [3:0] count;

  counter4 dut (
    .clk   (clk),
    .rst_n (rst_n),
    .en    (en),
    .count (count)
  );

  // Clock generator: 10 ns period (toggle every 5 ns)
  initial clk = 1'b0;
  always #5 clk = ~clk;

  // Waveform dump for GTKWave
  initial begin
    $dumpfile("counter4.vcd");
    $dumpvars(0, counter4_tb);
  end

  // Reset and stimulus
  initial begin
    $monitor("time=%0t  rst_n=%b en=%b count=%d", $time, rst_n, en, count);

    rst_n = 1'b0;  en = 1'b0;   // hold reset at start
    #12 rst_n = 1'b1;           // release reset between clock edges
    #8  en = 1'b1;              // start counting
    #60 en = 1'b0;              // pause the counter
    #20 en = 1'b1;              // resume
    #30 $finish;
  end
endmodule
VCD info: dumpfile counter4.vcd opened for output.
time=0  rst_n=0 en=0 count= 0
time=12  rst_n=1 en=0 count= 0
time=20  rst_n=1 en=1 count= 0
time=25  rst_n=1 en=1 count= 1
time=35  rst_n=1 en=1 count= 2
time=45  rst_n=1 en=1 count= 3
time=55  rst_n=1 en=1 count= 4
time=65  rst_n=1 en=1 count= 5
time=75  rst_n=1 en=1 count= 6
time=80  rst_n=1 en=0 count= 6
time=100  rst_n=1 en=1 count= 6
time=105  rst_n=1 en=1 count= 7
time=115  rst_n=1 en=1 count= 8
time=125  rst_n=1 en=1 count= 9
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpfv3yqw9m/counter_tb.v:47: $finish called at 130 (1s)

Read the log carefully: the count stays at 0 until reset is released and an enabled clock edge arrives, increments once per rising edge while en is high, freezes at 6 when en drops, and resumes from 6 — not from 0 — when en returns. Being able to predict a log like this before running it is exactly what interviewers probe.

Notice also that the three initial/always blocks all start at time 0 and run in parallel. A testbench is concurrent code, not a program that executes top to bottom.

Self-checking testbenches

Reading printed values by eye does not scale. A self-checking testbench computes the expected output itself (a “reference model”) and compares it with the DUT’s actual output, counting failures. This is the pattern real verification teams use, scaled up.

Three details matter in the example below:

  1. A task packages the apply–wait–compare sequence so each test case is one line.
  2. The comparison uses !== (case inequality) instead of !=. With !=, an x on either side makes the whole comparison unknown and the if silently skips the error branch — a bug in your DUT could hide behind an x. !== compares x and z literally.
  3. Directed corner cases (zero, overflow, all-ones) come first; random stimulus with $random adds breadth afterwards.
// Design under test: 4-bit adder with carry out
module adder4 (
  input  wire [3:0] a,
  input  wire [3:0] b,
  output wire [3:0] sum,
  output wire       cout
);
  assign {cout, sum} = a + b;
endmodule

// Self-checking testbench
module adder4_tb;
  reg  [3:0] a, b;
  wire [3:0] sum;
  wire       cout;

  integer errors;
  reg [4:0] expected;   // 5 bits: carry + sum

  adder4 dut (.a(a), .b(b), .sum(sum), .cout(cout));

  // Reusable check: apply inputs, wait, compare against the model
  task check;
    input [3:0] ta, tb;
    begin
      a = ta;  b = tb;
      #5;                       // let combinational logic settle
      expected = ta + tb;       // reference model
      if ({cout, sum} !== expected) begin
        errors = errors + 1;
        $display("FAIL: %d + %d -> got %d (cout=%b), expected %d",
                 ta, tb, sum, cout, expected);
      end else begin
        $display("PASS: %d + %d = %d cout=%b", ta, tb, sum, cout);
      end
    end
  endtask

  integer i;
  initial begin
    errors = 0;

    // Directed corner cases
    check(4'd0,  4'd0);
    check(4'd15, 4'd1);    // overflow into cout
    check(4'd15, 4'd15);   // maximum inputs
    check(4'd7,  4'd8);

    // A few random patterns
    for (i = 0; i < 4; i = i + 1)
      check($random, $random);

    if (errors == 0)
      $display("RESULT: all tests passed");
    else
      $display("RESULT: %0d test(s) FAILED", errors);
    $finish;
  end
endmodule
PASS:  0 +  0 =  0 cout=0
PASS: 15 +  1 =  0 cout=1
PASS: 15 + 15 = 14 cout=1
PASS:  7 +  8 = 15 cout=0
PASS:  4 +  1 =  5 cout=0
PASS:  9 +  3 = 12 cout=0
PASS: 13 + 13 = 10 cout=1
PASS:  5 +  2 =  7 cout=0
RESULT: all tests passed
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmple6p1rrc/adder_sc_tb.v:57: $finish called at 40 (1s)

The final RESULT line means a script — or a tired engineer at 2 a.m. — can judge the run with a single grep. Every serious testbench you write should end with an unambiguous verdict like this.

Checklist and common mistakes

  • Declared an input as wire? You cannot assign it in an initial block; make DUT inputs reg.
  • Clock stuck at x? You forgot to initialize it before toggling.
  • Simulation never ends? Missing $finish with a free-running clock.
  • No output at all? You wrote $monitor but used only one of several allowed monitors, or you never changed any monitored signal.
  • Checker never fails even on a broken DUT? You compared with != while signals carried x; use !== in checkers.
  • Empty VCD file? $dumpvars must name an existing scope (usually the testbench module) and run before signals start changing.
  • Reset released on a clock edge? Shift the release into the middle of a cycle to avoid a race.

Practice path: re-type the counter testbench from memory, then modify it — make the reset synchronous, add a load input, and extend the self-checking adder to 8 bits with 20 random vectors. Each change forces you through the structure again, and the structure is what interviews test.

Quick check

Why does a testbench module have no ports?

It is the top of the simulation hierarchy — nothing outside it drives or observes it. All stimulus is generated inside the testbench and all DUT outputs are observed inside it, so there is nothing to connect through ports.

Why are DUT inputs declared as reg and DUT outputs as wire in the testbench?

The testbench assigns DUT inputs inside initial/always blocks, and procedural assignment targets must be reg. DUT outputs are driven by the instantiated module’s output ports, and port-driven signals must be wire.

What goes wrong if you write “always #5 clk = ~clk;” without initializing clk?

clk starts as x (unknown), and ~x is still x, so the clock never produces real edges. You must set clk to 0 or 1 in an initial statement (or in the declaration) before the toggle takes effect.

Why should a self-checking testbench compare with !== instead of !=?

With !=, if either operand contains x or z the comparison result is unknown, the if condition is not true, and the error branch is silently skipped — hiding real bugs. The case inequality operator !== compares x and z values literally, so mismatches involving unknowns are still flagged.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.

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