@npw What exactly is it you‘re trying to test? Can you show an example? As there’ll be a couple of ways, but which approach would be “best” depends on what it is you’re actually trying to test.
Run Generic Tests against multiple models
Hi All,
I've written some generic phpunit tests, across a few test files which I would like PHPUnit/Pest to run against multiple different models. (The reasons for which are mainly to check that these have all been setup correctly).
I currently have these setup with a constructor in each test file, to which I pass the model name, for the purposes of writing the tests I just default the model name to a model that already exists (in this case 'Dummy').
Now i'm at the point whereby I want to run this set of tests against a number of different models programmatically, preferably in the normal way via phpunit/pest. But i'm not sure exactly the most sensible way to implement? I presumably need something similar to the following:
- Separate test file that loops through list of models
- Instantiates each of the test files and passes the model name
- runs the tests within the file
Anyone have any examples of something similar (doesn't have to be tests specifically), or any articles/videos that might be useful research material on how best/most sensibly, to accomplish this?
Thanks! N
Thanks @martinbean so its loosely separated into two types of tests:
- General simple configuration checks (Have permissions been defined, have routes been registered, has some other model specific config been setup). E.g.
public function __construct($name = null, array $data = [], $dataName = '', ?string $crf_model_name = 'Dummy')
{
parent::__construct($name, $data, $dataName);
$this->crf_model_name = $crf_model_name;
}
public function setUp() : void
{
parent::setUp();
$initUser = User::factory()->initial()->create();
$this->seed(RoleSeeder::class);
}
/**
* @test
*/
public function crf_permissions_defined(): void
{
$this->withoutExceptionHandling();
$defined_permissions = Permissions::options();
$required_permissions = ['View', 'Create', 'Update', 'Delete'];
$this->assertTrue(enum_exists(Permissions::class));
foreach ($required_permissions as $permission) {
$permission_name = $permission . $this->crf_model_name . 'CRF';
$permission_value = strtolower($permission) . ' ' . strtolower($this->crf_model_name) . ' crf';
$this->assertArrayHasKey($permission_name, $defined_permissions);
$this->assertEquals($defined_permissions['View' . $this->crf_model_name . 'CRF'], 'view ' . strtolower($this->crf_model_name) . ' crf');
}
}
- And then, a slightly more complex set of CRUD tests, E.g.:
// name of the crf Model to be used in tests
protected string $crf_model_name;
public function __construct($name = null, array $data = [], $dataName = '', ?string $crf_model_name = 'Dummy')
{
parent::__construct($name, $data, $dataName);
$this->crf_model_name = $crf_model_name;
}
public function setUp() : void
{
parent::setUp();
$initUser = User::factory()->initial()->create();
$this->seed(RoleSeeder::class);
}
/** @test */
public function users_with_permission_can_add_crf()
{
$this->withoutExceptionHandling();
$this->beUserWithPermission(collect(Permissions::cases())->firstWhere('name', 'Create' . $this->crf_model_name . 'CRF')->value);
$response = $this->get(route(strtolower($this->crf_model_name). '.create'));
$response->assertStatus(200);
$response->assertViewIs(strtolower($this->crf_model_name). '.create');
}
The reason for having these being, we have sets of models that are all very generic, and a team of devs who will be doing similar work, so this will hopefuily aid in checking things are setup in a consistent manner, and are easier to debug. Just so we ensure everyone has used the same optimistic locking/auditing/naming conventions etc.
Thanks for your help!
@npw You really shouldn’t be overriding the __construct method of test classes. The setUp method is for setting up anything for test cases.
If you want to run the same test case with different parameters, then you can use data providers for that:
class PermissionsTest extends TestCase
{
/**
* @dataProvider permissionsDataProvider
* @test
*/
public function permissionIsDefined(string $model, string $permission): void
{
// This case will be ran multiple times but with different parameters
// Add assertions here that expected permission was registered
}
public function permissionsDataProvider(): array
{
return [
[FooModel::class, 'foo permission name'],
[FooModel::class, 'bar permission name'],
[BarModel::class, 'foo permission name'],
[BarModel::class, 'bar permission name'],
];
}
}
@martinbean thanks Martin, yep i'm still roughing these out a bit, and have had a play with the data providers in a couple of tests (though they run before the setup method so its a bit awkward to generate anything dynamically using those, such as the list of models (don't want the dev to have to remember to add all the relevant models to the list)).
Thanks for the pointer on overriding the constructor, i'll look into moving everything into setup and check that still allows me to create an instance of a model on the fly.
But from what you are saying, am I right in thinking that you wouldn't call these tests from outside the test file itself?
You can use a data provider to test such simple stuff.
Something like this
<?php
namespace Tests\Feature\Http;
use Tests\TestCase;
class AuthorizationTest extends TestCase
{
public function protectedRoutesProvider()
{
return [
'Artists: a guest is not authorized to visit the create page' => ['get', '/artists/create'],
'Artists: a guest is not authorized to visit the edit page' => ['get', '/artists/1/edit'],
'Artists: a guest is not authorized to visit the store page' => ['post', '/artists'],
'Artists: a guest is not authorized to visit the update page' => ['put', '/artists/1'],
'Artists: a guest is not authorized to visit the delete page' => ['delete', '/artists/1'],
'Authors: a guest is not authorized to visit the create page' => ['get', '/authors/create'],
'Authors: a guest is not authorized to visit the edit page' => ['get', '/authors/1/edit'],
'Authors: a guest is not authorized to visit the store page' => ['post', '/authors'],
'Authors: a guest is not authorized to visit the update page' => ['put', '/authors/1'],
'Authors: a guest is not authorized to visit the delete page' => ['delete', '/authors/1'],
'Books: a guest is not authorized to visit the create page' => ['get', '/books/create'],
'Books: a guest is not authorized to visit the edit page' => ['get', '/books/1/edit'],
'Books: a guest is not authorized to visit the store page' => ['post', '/books'],
'Books: a guest is not authorized to visit the update page' => ['put', '/books/1'],
'Books: a guest is not authorized to visit the delete page' => ['delete', '/books/1'],
'Formats: a guest is not authorized to visit the create page' => ['get', '/formats/create'],
'Formats: a guest is not authorized to visit the edit page' => ['get', '/formats/1/edit'],
'Formats: a guest is not authorized to visit the store page' => ['post', '/formats'],
'Formats: a guest is not authorized to visit the update page' => ['put', '/formats/1'],
'Formats: a guest is not authorized to visit the delete page' => ['delete', '/formats/1'],
'Genres: a guest is not authorized to visit the create page' => ['get', '/genres/create'],
'Genres: a guest is not authorized to visit the edit page' => ['get', '/genres/1/edit'],
'Genres: a guest is not authorized to visit the store page' => ['post', '/genres'],
'Genres: a guest is not authorized to visit the update page' => ['put', '/genres/1'],
'Genres: a guest is not authorized to visit the delete page' => ['delete', '/genres/1'],
'Records: a guest is not authorized to visit the create page' => ['get', '/records/create'],
'Records: a guest is not authorized to visit the edit page' => ['get', '/records/1/edit'],
'Records: a guest is not authorized to visit the store page' => ['post', '/records'],
'Records: a guest is not authorized to visit the update page' => ['put', '/records/1'],
'Records: a guest is not authorized to visit the delete page' => ['delete', '/records/1'],
'Tracks: a guest is not authorized to visit the create page' => ['get', '/tracks/create'],
'Tracks: a guest is not authorized to visit the edit page' => ['get', '/tracks/1/edit'],
'Tracks: a guest is not authorized to visit the store page' => ['post', '/tracks'],
'Tracks: a guest is not authorized to visit the update page' => ['put', '/tracks/1'],
'Trackss: a guest is not authorized to visit the delete page' => ['delete', '/tracks/1'],
'BookCollection: a guest is not authorized to visit the store page' => ['post', '/bookcollections'],
'BookCollection: a guest is not authorized to visit the delete page' => ['delete', '/bookcollections/1'],
'BookRead: a guest is not authorized to visit the store page' => ['post', '/books/read'],
'BookRead: a guest is not authorized to visit the delete page' => ['delete', '/books/read/1'],
];
}
/**
* @test
* @dataProvider protectedRoutesProvider
* @param $method
* @param $route
*/
public function guests_are_redirected_to_login($method, $route)
{
$response = $this->$method($route);
$response->assertLocation('/login');
}
}
Apologies I think I haven't described this very well, or provided the best examples. I think the crux of what I am asking is can we use dependency injection in tests? And if so, any examples/materials people have found useful?
The above examples I provided are simpler, in some tests I am creating an instance of the model that I hope to inject, so that I can interact with it and call it's factory and things, not just check it's name against a string.
I appreciate we can cover a lot of the above using data providers, and think that they would cover my needs if the provider contents can be generated dynamically.
@npw Data providers. Either return a model instance in a closure:
public function testFoo(callable $modelBuilder): void
{
// Invoking the closure will build and return model instance
$modelInstance = $modelBuilder();
// Act...
// Assert...
}
public function fooDataProvider(): array
{
return [
'some case' => [
fn () => Foo::create(['foo' => 'bar']),
],
];
}
Or return attributes that you use to create a model in a test:
public function testFoo(array $attributes): void
{
$model = Foo::create($attributes);
// Act...
// Assert...
}
public function fooDataProvider(): array
{
return [
'some case' => [
['foo' => 'bar'],
],
];
}
@martinbean ah thankyou! That's really useful, closures in data provider might be the way to go then as you can use a factory that way. Thank you for your insight both, very much appreciated.
Found this article off the back of this, which may be useful for others... https://technicallyfletch.com/how-to-use-laravel-factories-inside-a-data-provider/
Please or to participate in this conversation.