Testing Without Excuses
Tags: tutorial, bash, testing, devops
Published: 2021-08-21 04:14:07

Every app has that last inch (or mile) of code that's not covered by tests.  Usually it's an interactive cycle of compile-run-inspect on the command line like 

## You Test
```
 curl -X POST https://reqbin.com/echo/post/json
```

##👀 You Expect:
```
{"success":"true"}
```

Despite having 3-4 testing frameworks for unit tests, e2e, regression etc-- there's always a gap where you find yourself re-playing commands in the terminal to test. 

A common case is 🔥firefighting where ad-hoc tests are needed to validate an emergency config change or deployment.

Not only is this a waste of time, it's error prone and reduces the number of assertions per run. 

With `Test::More`, you can easily run dozens of assertions in < 1 second.  Moreover, you can run them in a loop with `watch perl test.t` (read to the end to find out)

## Close the Testing Gap with Perl

To close this Gap, you want a basic testing framework to easily test assertions on the command line: asserting expected output or return status (e.g. success == 0, failure >= 1)

Perl Test::More is an elegant solution:
- it has a trivial and easy-to-remember interface: `ok()`, `is()`, `isnt()`
- it easily tests shell scripts like `head myfile.txt` to test output, or `system grep needle < haystack` to test return code
-  is universally available on every distro.  No apt-get, npm-install, downloading, linking, compiling needed.

## Let's Get Started

```
use Test::More tests => 1; # or

# basic test of stdout
is(`echo -n "hello dev.to"`, "hello dev.to", "test echo")  
```

The most basic `is()` test using actual backticks.  First arg is the command, second arg is expected output, third is a test description. 

### Testing Return Status Code Success
``` 
# test success status code = 0
is((system "echo hello dude | grep -q dude"), 0, "test output contains dude");
```

Similar to above, but use (system "COMMAND") to test the return status code instead of the output

### Test For Failure / Status code = 1
In this case, grep for "dude", assert that it fails (status=1)
```
# test failure status code = 1 (shift left by 8)
is((system "echo hello bob | grep -q dude"), 1<<8, "test output does not contain dude");
```
To test non-zero, bit-shift by 8 (read the docs for `system` to understand why)

### Test File Contents
```
# test reading a file
is(`head -n 1 /etc/passwd`, "root:x:0:0:root:/root:/bin/bash\n", 'top of passwd')
```
This pattern is useful, using head , tail or grep to test for file content.  Works well for testing log output upon running a command. 

### Testing APIs

```
# test curl output with special chars and newline
is(`curl -X POST https://reqbin.com/echo/post/json`, 
<<WANT
{"success":"true"}
WANT
, "test post req");
```

Use "heredoc" (multi-line doc) to test special characters & newlines without needing lots of escapes. 


### Run the Suite in the Background

```
use Test::More tests => 6; # or

# basic test of stdout
is(`echo -n "hello dev.to"`, "hello dev.to", "test echo");
# test success status code = 0
is((system "echo hello dude | grep -q dude"), 0, "test output contains dude");
# test failure status code = 1 (shift left by 8)
is((system "echo hello bob | grep -q dude"), 1<<8, "test output does not contain dude");
is((system "false"), 1<<8, "false not ok");
# test reading a file
is(`head -n 1 /etc/passwd`, "root:x:0:0:root:/root:/bin/bash\n", 'top of passwd');
# test curl output with special chars and newline
is(`curl -X POST https://reqbin.com/echo/post/json`, 
<<WANT
{"success":"true"}
WANT
, "test post req");
```

Now you have a suite of 6 tests, open a second terminal and run it with `watch perl test.t`
```
Every 2.0s: perl test.t                                                                                   pi4plus: Sat Aug 21 05:09:50 2021

1..6
ok 1 - test echo
ok 2 - test output contains dude
ok 3 - test output does not contain dude
ok 4 - false not ok
ok 5 - top of passwd
ok 6 - test post req
```

With Perl shell tests running in a loop, you can focus on code changes and your terminal will ring if a test case breaks.

## Wrap Up
I hope this helps you close that last gap of code that's not tested.  Even in the middle of a fire, you can copy-paste your tests into a `test.t` file within the terminal. 

Read the Perl Docs for More Info on [Test::More](https://perldoc.perl.org/Test::More) & [system](https://perldoc.perl.org/functions/system)