Part 2: Testing Model Relationships in Laravel — BASIC
100% code coverage is hardly achievable in code testing, we do aim closer but some tests are really boring. In this multipart post I helped you edge closer to 100% code coverage by writing some 5% of the tests you need in all your Laravel apps — the boring and often overlooked tests.
Eloquent relationships are ever present in every Laravel app but how do you ensure the expected models are all connected? This could be your cheatsheet for eloquent relationship tests. We are going to write tests that ensure all our models have relevant model connections and that they are correctly set.
- Part 1: Intro and Schema Tests
- Part 2: Testing Basic Model Relationships ⏯
- Part 3: Testing Polymorphic Relationships
THE RELATIONSHIPS
The word relationship is expected to involve at least two entities, thus every eloquent relationship has two parties, (a). The first relationship and (b). The Inverse relationship (either of the models could be made the first but a more influential or parent-esque model is okay as the first). As we write model relationship tests it is only right to test from the two concerned ends, thus we shall be writing for both first and inverse relationships.
- In mosts cases, relationship examples used in the official Laravel documentation will be used in this post. Your use-cases may be different, simply adapt the tests to your specific scenario.
- there are places where I included more than one method of testing, any of them is just fine.
- Watch out! Whenever a new model is introduced I add a schema test. Do not be surprised if some files have more test than others.
1. One To One Relationship
A one-to-one relationship is used to define relationships where a single model has a unary relationship with another model. Both models can own only one instance of the each other.
Scenario:
We have User and Phone models. A user is allowed to register only one phone number on the app, this means one unique phone entry for a given user in the phone table. This relationship can pan out as hasOne() and belongsTo() relationships.
hasOne()
— A user has one phone registered (FIRST).
// App/User.php
...
public function phone()
{
return $this->hasOne(Phone::class);
}
THE TEST: A user owns a phone, a phone is a unique entity thus we test that the phone is an instanceOf a Phone class. Second method tests existence of a relationship by asserting with a successful count.
belongsTo()
- — Any registered phone belongs to a single user
- — INVERSE of hasOne relationship.
// App/Phone.php
...
public function user()
{
return $this->belongsTo(User::class);
}
THE TEST: A phone belongs to a user, a user is a unique entity thus we tested that a user is an instanceOf a User class.
Files relevant to this test 👇
- MODEL FILES (App/)
-- User.php
-- Phone.php- MIGRATION FILES (database/migrations/)
-- 2014_10_12_000000_create_users_table.php
-- 2019_09_26_154439_create_phones_table.php- MODEL FACTORY FILES (database/factories/)
-- UserFactory.php
-- PhoneFactory.php- UNIT TEST FILES (tests/Units/)
-- UserTest.php
-- PhonesTest.php
Simply run the test suite and you should get a green.
$ pu or putu (or any nice alias you gave to your unit test commands)
or $ vendor/bin/phpunit
5 tests? Yeah, schema test inclusive. See the list below:
1. User schema test
2. Phone schema test
3. User->hasOne(phone) test (2 methods)
4. Phone->belongsTo(user) test
Tweak your code, goof around. Just have fun.
2. One To Many Relationship
A one-to-many relationship is used to define relationships where a single model owns any amount of other models. — Laravel.com
Scenario:
A blog post with an infinite number of comments or an author (user) with many articles or comments. We are going to write a test for Post and Comment models relationship. Thus this relationship can pan out as hasMany() and belongsTo() relationships.
hasMany()
— A post has many comments on it (FIRST).
// App/Post.php
...
public function comments()
{
return $this->hasMany(Comment::class);
}
THE TEST: Since a post has many comments, the comments are returned as collections on the post instance that is why we are able to do iterations when rendering results in app views. It is correct that we test a post’s comments as an instanceOf a collection.
belongsTo()
- — A comment belongs to a post
- — INVERSE of hasMany relationship.
// App/Comment.php
...
public function user()
{
return $this->belongsTo(Post::class);
}
THE TEST: A comment belongs to a post, a post is a unique entity thus we test that a post is an instanceOf a Post class. This is similar to $phone->user
test we had earlier.
Files relevant to this test 👇
- MODEL FILES (App/)
-- User.php
-- Post.php
-- Comment.php- MIGRATION FILES (database/migrations/)
-- 2014_10_12_000000_create_users_table.php
-- 2019_09_27_100604_create_posts_table.php
-- 2019_09_27_100620_create_comments_table.php- MODEL FACTORY FILES (database/factories/)
-- UserFactory.php
-- PostFactory.php
-- CommentFactory.php
- UNIT TEST FILES (tests/Units/)
-- UserTest.php
-- PostsTest.php
-- CommentsTest.php
If you run the test at this point it should all pass.
Heads up:
As depicted in the scenario section, this same One To Many tests can be replicated on User-Post, User-Comment and other model relationships that fit into the scenario (I have all the tests in this Github repo). A user can create many posts and many comments as well, these are hasMany() relationships. Writing model relationship tests will become natural to you when you fully understand how the eloquent relationships work.
3. Many-to-Many Relationship
A many-to-many relationship is used to define relationships where the two models involved can have many instances of each other.
Scenario:
A user can play many roles, and the roles are also shared by other users. Also for doctors and medical specialties, a specialty can be shared by many doctors while a doctor can be specialized in many specialties. Our test will be for User and Role models relationship.
belongsTo means a model that is already existing that another model fits into but have no privilege to create. This is different from hasOne or hasMany whose verb depict that a given model owns or creates another. On both models involved, the relationship is defined as belongsToMany().
belongsToMany()
— A user belongs to many roles (FIRST).
// App/User.php
...
public function roles()
{
return $this->belongsToMany(User::class);
}
belongsToMany()
- — A role belongs to many users
- — INVERSE of belongsToMany relationship. Kinda recursive.
// App/Role.php
...
public function users()
{
return $this->belongsToMany(User::class);
}
NB: role_user table must be migrated for this tests to pass.
Files relevant to this test 👇
- MODEL FILES (App/)
-- User.php
-- Role.php- MIGRATION FILES (database/migrations/)
-- 2014_10_12_000000_create_users_table.php
-- 2019_09_27_155312_create_roles_table.php
-- 2019_09_27_164342_create_role_user_table.php- MODEL FACTORY FILES (database/factories/)
-- UserFactory.php
-- RoleFactory.php
- UNIT TEST FILES (tests/Units/)
-- UserTest.php
-- RolesTest.php
4. Has One Through Relationship
The “has-one-through” relationship links models through a single intermediate relation.
Scenario:
Given a Supplier has one User, and each user is associated with a user History record, the supplier model may access the user’s history through the intermediate user model.
hasOneThrough()
- — A supplier has one history through a user
- — has no official INVERSE relationship.
// App/Supplier.php
...
public function userHistory()
{
return $this->hasOneThrough(History::class, User::class);
}
The “hasThrough” relationships do not have official reverse relationship since the concerned models are always indirectly related but a reverse can be hacked by linking through the intermediate model like below, however, beware of n+1 issues and eager load as appropriate.:
// App/History.php
...
/**
* With this attribute defined it can be used as $this->supplier
*/
public function getSupplierAttribute()
{
return $this->user->supplier;
}
Files relevant to this test 👇
- MODEL FILES (App/)
-- User.php
-- Supplier.php
-- History.php- MIGRATION FILES (database/migrations/)
-- 2014_10_12_000000_create_users_table.php
-- 2019_09_27_155312_create_roles_table.php
-- 2019_09_27_164342_create_role_user_table.php- MODEL FACTORY FILES (database/factories/)
-- UserFactory.php
-- SuppliersFactory.php
-- HistoryFactory.php
- UNIT TEST FILES (tests/Units/)
-- UserTest.php
-- SuppliersTest.php
-- HistoriesTest.php
5. Has Many Through Relationship
Just like the “has-one-through” relationship links a distant related model through an intermediate relation, “ has-many-through” does same but it can reference many instances of the distant related model.
Scenario:
A Country
model might have many Post
models through an intermediate User
model. We will write a test that ensures blog posts for a given country can be accessed on the country instance by a hasManyThrough relationship.
hasManyThrough()
- — A country can have many posts through a user model
- — has no official REVERSE relationship
// App/Country.php
...
public function posts()
{
return $this->hasManyThrough(Post::class, User::class);
}
NB: We now make use of a parent::setup()
method to DRY the test codes.
The “hasThrough” relationships do not have official reverse relationship but a reverse can be hacked by linking through the intermediate model like below, (beware of n+1 issues and eager load as appropriate):
// App/Post.php
...
/**
* With this attribute defined it can be used as $this->country
*/
public function getCountryAttribute()
{
return $this->user->country;
}
Files relevant to this test 👇
- MODEL FILES (App/)
-- User.php
-- Country.php
-- Post.php- MIGRATION FILES (database/migrations/)
-- 2014_10_12_000000_create_users_table.php
-- 2019_09_27_100604_create_posts_table.php
-- 2019_09_28_130218_create_countries_table.php
-- 2019_09_28_131328_add_country_id_to_users_table.php- MODEL FACTORY FILES (database/factories/)
-- UserFactory.php
-- CountryFactory.php
-- PostFactory.php
- UNIT TEST FILES (tests/Units/)
-- UserTest.php
-- CountriesTest.php
-- PostsTest.php
See the polymorphic relationship tests in the next post.
- 👉Part 1: Intro and Schema Tests
- ⏯Part 2: Testing Basic Model Relationships
- 👉Part 3: Testing Polymorphic Relationships