Datasets
Datasets allow you to define an array of test data, and Pest will run the same test once for each set automatically. This saves you time and effort, freeing you from repeating the same test by hand with different data. For example, consider the following test:
1it('has emails', function (string $email) {2 expect($email)->not->toBeEmpty();3})->with(['enunomaduro@gmail.com', 'other@example.com']);
When you run your tests, Pest will automatically add informative descriptions to any test that uses a dataset, outlining the parameters used in each case. This helps you understand the data at a glance and pinpoint the source if a test fails:
Of course, you may supply multiple arguments by providing an array that contains arrays of arguments:
1it('has emails', function (string $name, string $email) {2 expect($email)->not->toBeEmpty();3})->with([4 ['Nuno', 'enunomaduro@gmail.com'],5 ['Other', 'other@example.com']6]);
To add your own description to a dataset value, you may assign it a key:
1it('has emails', function (string $email) {2 expect($email)->not->toBeEmpty();3})->with([4 'james' => 'james@laravel.com',5 'taylor' => 'taylor@laravel.com',6]);
When a key is present, Pest will use it when generating the test's description:
If the test name includes :dataset, the description will be interpolated into the test name at that location:
1✓ it validates the "first_name" field2✓ it validates the "email" field
Note that when you use closures in your dataset, you must declare the argument types in the closure passed to the test function:
1it('can sum', function (int $a, int $b, int $result) {2 expect(sum($a, $b))->toBe($result);3})->with([4 'positive numbers' => [1, 2, 3],5 'negative numbers' => [-1, -2, -3],6 'using closure' => [fn () => 1, 2, 3],7]);
For larger or more complex scenarios, you may use closures:
1// Returning an array 2test('The array contains only integers', function ($i) { 3 expect($i)->toBeInt(); 4})->with(fn (): array => range(1, 99)); 5 6// Using a generator 7test('The generator produces only integers', function ($i) { 8 expect($i)->toBeInt(); 9})->with(function (): Generator {10 for ($i = 1 ; $i < 100_000_000_000 ; $i++) {11 yield $i;12 }13});
Named Parameters
When you use datasets with associative arrays, Pest matches the dataset keys to the closure's parameter names, regardless of order. This allows you to define your dataset in any key order, and the values will be mapped to the correct parameters automatically:
1it('has user data', function (string $email, string $name) {2 expect($name)->toBeString();3 expect($email)->toContain('@');4})->with([5 ['name' => 'Taylor', 'email' => 'taylor@laravel.com'],6 ['name' => 'Nuno', 'email' => 'enunomaduro@gmail.com'],7]);
As you can see, even though the dataset defines name before email, Pest maps them correctly to the closure parameters $email and $name.
Named parameters also work with shared datasets and bound closures:
1dataset('users', [2 ['name' => 'Taylor', 'email' => 'taylor@laravel.com'],3 ['name' => 'Nuno', 'email' => 'enunomaduro@gmail.com'],4]);5 6it('has user data', function (string $email, string $name) {7 expect($name)->toBeString();8 expect($email)->toContain('@');9})->with('users');
Bound Datasets
Pest's bound datasets allow you to obtain a dataset that is resolved after the beforeEach() method of your tests has run. This is particularly helpful in Laravel applications (or any other Pest integration) where you may need a dataset of App\Models\User models created after your database schema is prepared by the beforeEach() method:
1it('can generate the full name of a user', function (User $user) {2 expect($user->full_name)->toBe("{$user->first_name} {$user->last_name}");3})->with([4 fn() => User::factory()->create(['first_name' => 'Nuno', 'last_name' => 'Maduro']),5 fn() => User::factory()->create(['first_name' => 'Luke', 'last_name' => 'Downing']),6 fn() => User::factory()->create(['first_name' => 'Freek', 'last_name' => 'Van Der Herten']),7]);
If you wish, you may bind a single argument to the test case. However, Pest requires that it be fully typed in the it|test function arguments:
1-it('can generate the full name of a user', function ($user, $fullName) {2+it('can generate the full name of a user', function (User $user, $fullName) {3 expect($user->full_name)->toBe($fullName);4})->with([5 [fn() => User::factory()->create(['first_name' => 'Nuno', 'last_name' => 'Maduro']), 'Nuno Maduro'],6 [fn() => User::factory()->create(['first_name' => 'Luke', 'last_name' => 'Downing']), 'Luke Downing'],7 [fn() => User::factory()->create(['first_name' => 'Freek', 'last_name' => 'Van Der Herten']), 'Freek Van Der Herten'],8]);
Sharing Datasets
By storing your datasets separately in the tests/Datasets folder, you may keep them distinct from your test code and ensure they do not clutter your main test files:
1// tests/Unit/ExampleTest.php... 2it('has emails', function (string $email) { 3 expect($email)->not->toBeEmpty(); 4-})->with(['enunomaduro@gmail.com', 'other@example.com']); 5+})->with('emails'); 6 7// tests/Datasets/Emails.php... 8+dataset('emails', [ 9+ 'enunomaduro@gmail.com',10+ 'other@example.com'11+]);
Bound datasets, description keys, and the other rules that apply to inline datasets may also be applied to shared datasets.
Scoped Datasets
Sometimes you may have datasets that pertain only to a specific feature or set of folders. In such cases, rather than distributing the dataset globally within the Datasets folder, you may create a Datasets.php file within the folder that requires the dataset, restricting the dataset's scope to that folder alone:
1// tests/Feature/Products/ExampleTest.php... 2it('has products', function (string $product) { 3 expect($product)->not->toBeEmpty(); 4})->with('products'); 5 6// tests/Feature/Products/Datasets.php... 7dataset('products', [ 8 'egg', 9 'milk'10]);
Combining Datasets
You may obtain complex datasets by combining both inline and shared datasets. When you do, the datasets will be combined using a cartesian product approach.
In the following example, we verify that each of the specified businesses is closed on every one of the provided weekdays:
1dataset('days_of_the_week', [ 2 'Saturday', 3 'Sunday', 4]); 5 6test('business is closed on day', function(string $business, string $day) { 7 expect(new $business)->isClosed($day)->toBeTrue(); 8})->with([ 9 Office::class,10 Bank::class,11 School::class12])->with('days_of_the_week');
When you run the example above, Pest's output will contain a description of each validated combination:
Describe Blocks With Datasets
You may attach a dataset to a describe() block, and every test within that block will receive the dataset values:
1describe('user notifications', function () {2 test('can send notification', function (string $channel) {3 expect($channel)->toBeString();4 });5 6 test('can queue notification', function (string $channel) {7 expect($channel)->toBeIn(['mail', 'sms']);8 });9})->with(['mail', 'sms']);
You may also use beforeEach()->with() inside a describe() block to apply a dataset to all tests within that scope:
1describe('user settings', function () {2 beforeEach()->with([10, 20, 30]);3 4 test('receives the dataset value', function (int $value) {5 expect($value)->toBeGreaterThan(0);6 });7});
Repeating Tests
Sometimes you may need to repeat a test multiple times, whether for debugging purposes or to ensure that it is stable. On these occasions, you may use the repeat() method to run a test a given number of times:
1it('can repeat a test', function () {2 $result = /** Some code that may be unstable */;3 4 expect($result)->toBeTrue();5})->repeat(100); // Repeat the test 100 times
Now that you are comfortable using datasets in your tests, the next step is to learn how to test for exceptions, verifying that your code behaves correctly and throws the appropriate exceptions when it encounters unexpected or erroneous input: Exceptions →