BACKGROUND
I've been having lots of issues with unit testing and I think a lot of it has to do with sqlite. Since my local and production environments are both mysql, I'd like to test in as similar conditions as possible.
I'm using Laravel Sail, and I found this guide on how to create a test database in a separate docker container for testing.
I followed the steps and after working through a couple of initial errors I got it working...kind of. All my tests are now failing, but I think it's more to do with how I set up my tests, or some misunderstanding on my part.
The TL;DR on the guide I mentioned above is you add a record to your phpunit.xml file, so now mine looks like this:
<php>
<server name="APP_ENV" value="testing"/>
<server name="BCRYPT_ROUNDS" value="4"/>
<!-- <server name="DB_CONNECTION" value="testing"/> <--guide says you don't need
<server name="DB_DATABASE" value="testing"/> --> <--guide says you don't need.
<server name="CACHE_DRIVER" value="array"/>
<server name="MAIL_DRIVER" value="array"/>
<server name="QUEUE_CONNECTION" value="sync"/>
<server name="SESSION_DRIVER" value="array"/>
<env name="DB_HOST" value="testdb"/> <--this is what's new
</php>
Then you modify your docker-compose.yml file, which I did, and mine now looks like this:
mysql:
image: 'mysql/mysql-server:8.0'
ports:
- '${FORWARD_DB_PORT:-3306}:3306'
environment:
MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
MYSQL_ROOT_HOST: "%"
MYSQL_DATABASE: '${DB_DATABASE}'
MYSQL_USER: '${DB_USERNAME}'
MYSQL_PASSWORD: '${DB_PASSWORD}'
MYSQL_ALLOW_EMPTY_PASSWORD: 1
volumes:
- 'sail-mysql:/var/lib/mysql'
networks:
- sail
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-p${DB_PASSWORD}"]
retries: 3
timeout: 5s
testdb:
image: 'mysql/mysql-server:8.0'
tmpfs: /var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
MYSQL_ROOT_HOST: "%"
MYSQL_DATABASE: '${DB_DATABASE}'
MYSQL_USER: '${DB_USERNAME}'
MYSQL_PASSWORD: '${DB_PASSWORD}'
MYSQL_ALLOW_EMPTY_PASSWORD: 1
networks:
- sail
I suspect that part is not the issue, but I'm putting it out there for background.
FAILURE
All my tests (except the first one) are failing with the same error when attempting to insert into the users table. The users table has a role_id foreign key, which from the error, it seems like this table isn't populated when the user factory is called. Here's the error:
SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails
TESTING DETAILS
I'm using the RefreshDatabase trait in Laravel, and all of my tests start with a setup method. They all closely resemble this:
public function setup(): void
{
parent::setUp();
$this->setupFaker();
$this->seed();
$this->user = User::factory()->create();
$this->client = Client::factory()->create();
$this->location = Location::factory()->create();
}
I have one main DatabaseSeeder file that sets up all the reference data/lookup tables and stuff that doesn't change but is needed. The file contains this run method:
public function run()
{
Schema::disableForeignKeyConstraints();
$this->call([
RoleSeeder::class, <--roles are seeded here
LocationStatusSeeder::class,
TaskStatusSeeder::class,
TaskTypeSeeder::class,
UserSeeder::class,
USStateSeeder::class,
FrequencyTypeSeeder::class,
SmsTypeSeeder::class,
TransactionTypeSeeder::class,
DefaultTaskSeeder::class,
]);
Schema::enableForeignKeyConstraints();
}
My understanding is that when the setup method runs, it calls $this->seed(), which ultimately runs the DatabaseSeeder class.
Here's a bigger snippet of my ClientTest file. The test_client_page_is_rendered passes, but all the other tests in the file fail with the above error. This is also the first set of tests in the overall testing, so something is allowing it to work for the first test and fail every other test:
protected $user;
protected $client;
protected $location;
public function setup(): void
{
parent::setUp();
$this->setupFaker();
$this->seed();
$this->user = User::factory()->create();
$this->client = Client::factory()->create();
$this->location = Location::factory()->create();
}
public function test_client_page_is_rendered()
{
$user = User::first();
$client = Client::first();
$this->actingAs($user);
$response = $this->get('/clients', ['id' => $client['id']]);
$response->assertStatus(200);
}
public function test_client_can_be_created()
{
$user = User::first();
Livewire::actingAs($user);
$formData = [
'client.company_name' => $this->faker->company,
'client.address' => $this->faker->streetAddress,
'client.city' => $this->faker->city,
'client.state' => $this->faker->StateAbbr,
'client.zip_code' => $this->faker->numerify('#####'),
];
$client = Livewire::test(ClientCreateOrUpdate::class, $formData)->call('save');
$this->assertNotNull($client);
}
Previously I was referencing $this->client, etc in my testing methods, but had switched to just getting the first record as an attempt to work through this. That's why I have properties in the setup method that are assigned but not used in the test methods.
I know with the in-memory sqlite every test was essentially re-populating the database, but I don't think that's how it works now that I've switched it, so something about my setup needs to be changed, I just haven't been able to figure out what it is.