DISCLAIMER: Image is generated using ChatGPT.
If you’re a fan of Test Driven Development (TDD) like I am, you have likely explored many different ways to test your code. I’m not ashamed to call myself an old school student; for a long time, I relied on my good old friend, Test::More, for most of my testing.
However, a few years ago, I started using Test2::V0. I’ve seen our team at work, upgrade our unit tests to Test2::V0, and one particular change that stood out to me is the default deep checking. Specifically, the fact that is() now handles everything is_deeply() used to do.
Let’s set the stage for today’s walkthrough:
$ cpanm -vS Test2::Suite Test2::Harness
We will create very basic unit test, t/demo1.t, using Test::More first:
use Test::More;
ok(1, "Basic truth check");
is_deeply(
{ name => 'Joe', age => 41 },
{ name => 'Joe', age => 41 },
"Hash structures match exactly"
);
like("Hello World", qr/Hello/, "Regex check works");
done_testing;
In the Perl world, prove is the classic command-line tool used to run tests. It acts as a wrapper around Test::Harness.
$ prove
t/demo1.t .. ok
All tests successful.
Files=1, Tests=3, 0 wallclock secs ( 0.01 usr 0.00 sys + 0.04 cusr 0.00 csys = 0.05 CPU)
Result: PASS
So far so good, right?
The creator of Test::Harness came up with modern alternative such as Test2::Harness. With this, we got another command-line tool yath.
So how do you use yath instead of prove?
$ yath
PERL_HASH_SEED not set, setting to '20260426' for more reproducible results.
** Defaulting to the 'test' command **
( PASSED ) job 1 t/demo1.t
Yath Result Summary
-----------------------------------------------------------------------------------
File Count: 1
Assertion Count: 3
Wall Time: 0.48 seconds
CPU Time: 0.43 seconds (usr: 0.12s | sys: 0.03s | cusr: 0.21s | csys: 0.07s)
CPU Usage: 90%
--> Result: PASSED <--
Since we are using the modern framework, why not upgrade the unit test, t/demo2.t, to use Test2::V0.
use Test2::V0;
ok(1, "Basic truth check");
is(
{ name => 'Joe', age => 41 },
{ name => 'Joe', age => 41 },
"Hash structures match exactly"
);
like("Hello World", qr/Hello/, "Regex check works");
done_testing;
Now run the test using yath.
$ yath
PERL_HASH_SEED not set, setting to '20260426' for more reproducible results.
** Defaulting to the 'test' command **
( PASSED ) job 1 t/demo1.t
( PASSED ) job 2 t/demo2.t
Yath Result Summary
-----------------------------------------------------------------------------------
File Count: 2
Assertion Count: 6
Wall Time: 0.37 seconds
CPU Time: 0.45 seconds (usr: 0.09s | sys: 0.02s | cusr: 0.26s | csys: 0.08s)
CPU Usage: 123%
--> Result: PASSED <--
The magic of yath is in its subcommands.
$ yath test
PERL_HASH_SEED not set, setting to '20260426' for more reproducible results.
( PASSED ) job 1 t/demo1.t
( PASSED ) job 2 t/demo2.t
Yath Result Summary
-----------------------------------------------------------------------------------
File Count: 2
Assertion Count: 6
Wall Time: 0.41 seconds
CPU Time: 0.42 seconds (usr: 0.10s | sys: 0.03s | cusr: 0.22s | csys: 0.07s)
CPU Usage: 103%
--> Result: PASSED <--
It doesn’t stop there, try this now:
$ yath test -v
PERL_HASH_SEED not set, setting to '20260426' for more reproducible results.
( LAUNCH ) job 1 t/demo1.t
[ PASS ] job 1 + Basic truth check
[ PASS ] job 1 + Hash structures match exactly
[ PASS ] job 1 + Regex check works
[ PLAN ] job 1 Expected assertions: 3
( MEMORY ) job 1 rss: 29112kB
( MEMORY ) job 1 size: 33580kB
( MEMORY ) job 1 peak: 33584kB
( PASSED ) job 1 t/demo1.t
( TIME ) job 1 Startup: 0.03667s | Events: 0.00035s | Cleanup: 0.00273s | Total: 0.03975s
( LAUNCH ) job 2 t/demo2.t
( NOTE ) job 2 Seeded srand with seed '20260426' from local date.
[ PASS ] job 2 + Basic truth check
[ PASS ] job 2 + Hash structures match exactly
[ PASS ] job 2 + Regex check works
[ PLAN ] job 2 Expected assertions: 3
( MEMORY ) job 2 rss: 31200kB
( MEMORY ) job 2 size: 35196kB
( MEMORY ) job 2 peak: 35196kB
( PASSED ) job 2 t/demo2.t
( TIME ) job 2 Startup: 0.04264s | Events: 0.00068s | Cleanup: 0.00262s | Total: 0.04594s
Yath Result Summary
-----------------------------------------------------------------------------------
File Count: 2
Assertion Count: 6
Wall Time: 0.71 seconds
CPU Time: 0.41 seconds (usr: 0.11s | sys: 0.01s | cusr: 0.23s | csys: 0.06s)
CPU Usage: 57%
--> Result: PASSED <--
One of the best feature of yath is the persistent runner. It pre-loads your environment so tests start instantly:
Start the runner : yath start
Run tests against it: yath run
Stop it when done : yath stop
So far, we have seen tests running sequentially. How about running the test in parallel?
For that, lets create some more test to demo this feature.
$ mkdir t/parallel
$ for i in {1..4}; do
cat <<EOF > t/parallel/test_$i.t
use Test2::V0;
use Time::HiRes qw(time);
my \$start_time = time();
my \$pid = \$\$;
diag("Job $i started at \$start_time on PID \$pid");
sleep 2;
ok(1, "Job $i finished");
done_testing;
EOF
done
This is what we got.
$ tree
.
└── t
├── demo1.t
├── demo2.t
└── parallel
├── test_1.t
├── test_2.t
├── test_3.t
└── test_4.t
3 directories, 6 files
Can we use prove to run the test in parallel?
Yes, we can.
$ prove -v t/parallel/ -j4
# Job 1 started at 1777243707.02666 on PID 244346
===( 1;0 1/? 0/? 0/? 0/? )====================================# Job 2 started at 1777243707.02893 on PID 244347
===( 2;0 1/? 1/? 0/? 0/? )====================================# Job 3 started at 1777243707.03 on PID 244348
===( 3;0 1/? 1/? 1/? 0/? )====================================# Job 4 started at 1777243707.03165 on PID 244349
===( 4;0 1/? 1/? 1/? 1/? )====================================ok
===( 4;0 1/1 1/? 1/? )=========================================ok
===( 4;0 1/? 1/? )==============================================ok
t/parallel/test_4.t .. ok
All tests successful.
Files=4, Tests=4, 3 wallclock secs ( 0.01 usr 0.00 sys + 0.15 cusr 0.01 csys = 0.17 CPU)
Result: PASS
Now try yath as below:
$ yath test t/parallel/ -v
PERL_HASH_SEED not set, setting to '20260426' for more reproducible results.
( LAUNCH ) job 1 t/parallel/test_1.t
( NOTE ) job 1 Seeded srand with seed '20260426' from local date.
( DIAG ) job 1 Job 1 started at 1777241712.9551 on PID 240015
[ PASS ] job 1 + Job 1 finished
[ PLAN ] job 1 Expected assertions: 1
( MEMORY ) job 1 rss: 31164kB
( MEMORY ) job 1 size: 35200kB
( MEMORY ) job 1 peak: 35200kB
( PASSED ) job 1 t/parallel/test_1.t
( TIME ) job 1 Startup: 0.07473s | Events: 2.00068s | Cleanup: 0.00319s | Total: 2.07860s
( LAUNCH ) job 2 t/parallel/test_2.t
( NOTE ) job 2 Seeded srand with seed '20260426' from local date.
( DIAG ) job 2 Job 2 started at 1777241715.02359 on PID 240016
[ PASS ] job 2 + Job 2 finished
[ PLAN ] job 2 Expected assertions: 1
( MEMORY ) job 2 rss: 31200kB
( MEMORY ) job 2 size: 35140kB
( MEMORY ) job 2 peak: 35148kB
( PASSED ) job 2 t/parallel/test_2.t
( TIME ) job 2 Startup: 0.04366s | Events: 2.00074s | Cleanup: 0.00350s | Total: 2.04790s
( LAUNCH ) job 3 t/parallel/test_3.t
( NOTE ) job 3 Seeded srand with seed '20260426' from local date.
( DIAG ) job 3 Job 3 started at 1777241717.09132 on PID 240017
[ PASS ] job 3 + Job 3 finished
[ PLAN ] job 3 Expected assertions: 1
( MEMORY ) job 3 rss: 31204kB
( MEMORY ) job 3 size: 35108kB
( MEMORY ) job 3 peak: 35108kB
( PASSED ) job 3 t/parallel/test_3.t
( TIME ) job 3 Startup: 0.04252s | Events: 2.00085s | Cleanup: 0.00329s | Total: 2.04666s
( LAUNCH ) job 4 t/parallel/test_4.t
( NOTE ) job 4 Seeded srand with seed '20260426' from local date.
( DIAG ) job 4 Job 4 started at 1777241719.15869 on PID 240018
[ PASS ] job 4 + Job 4 finished
[ PLAN ] job 4 Expected assertions: 1
( MEMORY ) job 4 rss: 31204kB
( MEMORY ) job 4 size: 35128kB
( MEMORY ) job 4 peak: 35128kB
( PASSED ) job 4 t/parallel/test_4.t
( TIME ) job 4 Startup: 0.04226s | Events: 2.00090s | Cleanup: 0.00369s | Total: 2.04685s
Yath Result Summary
-----------------------------------------------------------------------------------
File Count: 4
Assertion Count: 4
Wall Time: 8.79 seconds
CPU Time: 8.71 seconds (usr: 0.11s | sys: 0.06s | cusr: 6.88s | csys: 1.66s)
CPU Usage: 99%
--> Result: PASSED <--
While the prove -j could do this, yath is the preferred choice because of Job Sorting. If you have a massive suite, yath tracks which tests take the longest and runs them first in the next run.
I have only scratched the surface, if you want to explore further then take a look at this App::Yath.
Happy Hacking !!!
