App-Test-Generator
view release on metacpan or search on metacpan
doc/getting-started-blog.md view on Meta::CPAN
# Stop Writing Test Cases By Hand: An Introduction to Specification-Driven Testing in Perl
**TL;DR:** Imagine describing what your function *should do* once, and automatically generating hundreds of test cases that probe edge cases, boundary conditions, and error handling. That's `App::Test::Generator`.
## The Problem
You've just written a function that validates email addresses. You know you should test it thoroughly:
```perl
sub validate_email {
my ($email) = @_;
# ... validation logic ...
return 1 if valid, dies otherwise
}
```
So you start writing tests:
```perl
ok(validate_email('user@example.com'), 'basic email works');
ok(validate_email('user+tag@example.com'), 'plus addressing works');
dies_ok { validate_email('') } 'empty string dies';
dies_ok { validate_email('not-an-email') } 'invalid format dies';
# ... 50 more test cases you need to think of ...
```
**The questions that keep you up at night:**
- Did I test empty strings? What about strings with only whitespace?
- What about null bytes? Unicode? Emoji?
- What if someone passes `undef`? An array reference?
- Did I check the boundary between valid and invalid lengths?
- Am I testing the same cases I tested last month, or did I miss something new?
## The Solution: Write a Specification, Get Tests for Free
Instead of writing individual test cases, describe what your function *should accept*:
```yaml
---
module: Email::Validator
function: validate_email
input:
email:
type: string
min: 3
max: 254 # RFC 5321 limit
matches: "^[\\w.+-]+@[\\w.-]+\\.[a-zA-Z]{2,}$"
output:
type: boolean
config:
test_undef: yes
test_empty: yes
test_nuls: yes
seed: 42
iterations: 100
```
Save this as `t/conf/validate_email.yml`, then run:
```bash
$ fuzz-harness-generator t/conf/validate_email.yml > t/validate_email_fuzz.t
$ prove -v t/validate_email_fuzz.t
```
**What just happened?**
The generator created a test file with:
- â
100 random valid emails (fuzzing)
- â
Edge cases: exactly 3 chars, exactly 254 chars, 2 chars (too short), 255 chars (too long)
- â
Regex boundary tests: strings that almost match, strings with special chars
- â
Invalid inputs: `undef`, empty string, null bytes, emoji, full-width characters
- â
Wrong types: arrays, hashes, numbers passed as email
- â
Reproducible: Same seed = same random tests every time
That's **200+ test cases** from a 15-line YAML file.
## Real-World Example: Testing a Math Function
Let's test something more complex. Here's a function that normalizes numbers to a 0-1 range:
```perl
package Math::Utils;
sub normalize {
my ($value, $min, $max) = @_;
die "min must be less than max" unless $min < $max;
die "value out of range" if $value < $min || $value > $max;
return ($value - $min) / ($max - $min);
}
```
The specification captures both valid inputs **and** the transformation rules:
```yaml
---
module: Math::Utils
function: normalize
input:
value:
type: number
position: 0
min:
type: number
position: 1
max:
type: number
position: 2
output:
type: number
min: 0
max: 1
transforms:
min_value_returns_zero:
input:
value: { type: number, value: 0 }
min: { type: number, value: 0 }
max: { type: number, value: 100 }
output:
type: number
value: 0
max_value_returns_one:
input:
value: { type: number, value: 100 }
min: { type: number, value: 0 }
max: { type: number, value: 100 }
output:
type: number
value: 1
midpoint_returns_half:
input:
value: { type: number, value: 50 }
min: { type: number, value: 0 }
max: { type: number, value: 100 }
output:
type: number
value: 0.5
inverted_range_dies:
input:
value: { type: number, value: 50 }
min: { type: number, value: 100 }
max: { type: number, value: 0 }
output:
_STATUS: DIES
iterations: 50
seed: 42
```
This generates tests that verify:
1. The math is correct (transforms)
2. Boundary conditions work (min=0 returns 0, max=100 returns 1)
3. Invalid inputs are rejected (inverted range dies)
4. Random inputs within range work correctly
## The Five-Minute Quick Start
### 1. Install the module
```bash
cpanm App::Test::Generator
```
### 2. Create a configuration file
`t/conf/my_function.yml`:
```yaml
---
module: My::Module
function: my_function
input:
name:
type: string
min: 1
max: 100
age:
type: integer
min: 0
max: 150
optional: true
output:
type: string
seed: 12345
iterations: 50
```
### 3. Generate and run tests
```bash
# Generate the test file
fuzz-harness-generator t/conf/my_function.yml > t/my_function_fuzz.t
# Run it
prove -v t/my_function_fuzz.t
```
### 4. Add to your CI/CD
The best part? Add this to GitHub Actions and it runs automatically:
`.github/workflows/fuzz.yml`:
```yaml
name: Fuzz Testing
on:
push:
branches: [main, master]
schedule:
- cron: '0 2 * * *' # Daily at 2 AM
jobs:
fuzz-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Setup Perl
uses: shogo82148/actions-setup-perl@v1
with:
perl-version: '5.38'
- name: Install dependencies
run: |
cpanm App::Test::Generator
cpanm --installdeps .
- name: Generate and run fuzz tests
run: |
mkdir -p t/fuzz
for config in t/conf/*.yml; do
test_name=$(basename "$config" .yml)
fuzz-harness-generator "$config" > "t/fuzz/${test_name}_fuzz.t"
done
prove -lr t/fuzz/
```
Now you get **continuous fuzzing** - your code is tested against hundreds of edge cases every day.
## Common Patterns
### Pattern 1: Security-Critical Input Validation
```yaml
doc/getting-started-blog.md view on Meta::CPAN
```yaml
type_edge_cases:
string:
- ''
- ' '
- "\t\n"
- 'x' x 10000
- "ðð" # emoji
integer:
- 0
- -1
- 2147483647 # 32-bit max
- -2147483648 # 32-bit min
```
## Getting Help
- ð **Full documentation**: `perldoc App::Test::Generator`
- ð **Issues/Questions**: https://github.com/nigelhorne/App-Test-Generator/issues
- ð¬ **Examples**: See `t/conf/` in the repository
- ð **Coverage reports**: https://nigelhorne.github.io/App-Test-Generator/coverage/
## Try It Today
Pick one function in your codebase that:
1. Has clear input requirements
2. You're nervous about (parsing, validation, critical path)
3. Could use more test coverage
Write a 10-line YAML file describing it. Generate 200+ tests. Find bugs you didn't know existed.
That's the power of specification-driven testing.
---
*App::Test::Generator is open source and available on CPAN. Contributions welcome!*
---
## Appendix: Complete Example
Here's a complete example testing CGI::Info's `script_path` function (which takes no arguments and returns an absolute path):
`t/conf/script_path.yml`:
```yaml
---
module: CGI::Info
function: script_path
input: undef # Takes no arguments
output:
type: string
min: 1
matches: "^(?:[A-Za-z]:[/\\\\]|/)" # Windows or Unix absolute path
config:
test_undef: yes
seed: 42
iterations: 50
```
Generated test output:
```
ok 1 - use CGI::Info;
ok 2 - script_path survives
ok 3 - output validates
ok 4 - script_path survives
ok 5 - output validates
# ... 50+ tests of calling with no arguments ...
1..52
```
Simple, clear, comprehensive.
( run in 1.157 second using v1.01-cache-2.11-cpan-96521ef73a4 )