I'm using Laravel for my project and I'm new to unit/feature testing so I was wondering what is the best way to approach more complicated feature cases when writing tests?
Let's take this test example:
// tests/Feature/UserConnectionsTest.php
public function testSucceedIfConnectAuthorised()
{
$connection = factory(Connection::class)->make([
'sender_id' => 1,
'receiver_id' => 2,
'accepted' => false,
'connection_id' => 5,
]);
$user = factory(User::class)->make([
'id' => 1,
]);
$response = $this->actingAs($user)->post(
'/app/connection-request/accept',
[
'accept' => true,
'request_id' => $connection->id,
]
);
$response->assertLocation('/')->assertStatus(200);
}
So we've got this situation where we have some connection system between two users. There is a Connection entry in the DB created by one of the users. Now to make it a successful connection the second user has to approve it. The problem is within the UserController accepting this through connectionRequest:
// app/Http/Controllers/Frontend/UserController.php
public function connectionRequest(Request $request)
{
// we check if the user isn't trying to accept the connection
// that he initiated himself
$connection = $this->repository->GetConnectionById($request->get('request_id'));
$receiver_id = $connection->receiver_id;
$current_user_id = auth()->user()->id;
if ($receiver_id !== $current_user_id) {
abort(403);
}
[...]
}
// app/Http/Repositories/Frontend/UserRepository.php
public function GetConnectionById($id)
{
return Connection::where('id', $id)->first();
}
So we've got this fake (factory created) connection in the test function and then we unfortunately are using its fake id to run a check within the real DB among real connections, which is not what we want :(
Researching I found an idea of creating interfaces so then we can provide a different method bodies depending if we're testing or not. Like here for GetConnectionById() making it easy to fake answers to for the testing case. And that seems OK, but:
for one it looks like a kind of overhead, besides writing tests I have to make the "real" code more complicated itself for the sole purpose of testing.
and second thing, I read all that Laravel documentation has to say about testing, and there is no one place where they mention using of interfaces, so that makes me wonder too if that's the only way and the best way of solving this problem.
I will try to help you, when someone start with testing it is not easy at all, specially if you don't have a strong framework (or even a framework at all).
So, let me try help you:
It is really important to differentiate Unit testing vs Feature testing. You are correctly using Feature test, because you want to test business logic instead of a class directly.
When you test, my personal recommendation is always create a second DB to only use with tests. It must be completely empty all the time.
So, for you to achieve this, you have to define the correct environment variables in phpunit.xml, so you don't have to do magic for this to work when you only run tests.
Also, use RefreshDatabase trait. So, each time you run a test, it is going to delete everything, migrate your tables again and run the test.
You should always create what you need to have as mandatory for your test to run. For example, if you are testing if a user can cancel an order he/she created, you only need to have a product, a user and an invoice associated with the product and user. You do not need to have notifications created or anything not related to this. You must have what you expect to have in the real case scenario, but nothing extra, so you can truly test that it fully works with the minimum stuff.
You can run seeders if your setup is "big", so you should be using setup method.
Remember to NEVER mock core code, like request or controllers or anything similar. If you are mocking any of these, you are doing something wrong. (You will learn this with experience, once you truly know how to test).
When you write tests names, remember to never use if and must and similar wording, instead use when and should. For example, your test testSucceedIfConnectAuthorised should be named testShouldSucceedWhenConnectAuthorised.
This tip is super personal: do not use RepositoryPattern in Laravel, it is an anti-pattern. It is not the worst thing to use, but I recommend having a Service class (do not confuse with a Service Provider, the class I mean is a normal class, it is still called Service) to achieve what you want. But still, you can google about this and Laravel and you will see everyone discourages this pattern in Laravel.
One last tip, Connection::where('id', $id)->first() is exactly the same as Connection::find($id).
I forgot to add that, you should always hardcode your URLs (like you did in your test) because if you rely on route('url.name') and the name matches but the real URL is /api/asdasdasd, you will never test that the URL is the one you want. So congrats there ! A lot of people do not do this and that is wrong.
So, to help you in your case, I will assume you have a clear database (database without tables, RefreshDatabase trait will handle this for you).
I would have your first test as this:
public function testShouldSucceedWhenConnectAuthorised()
{
/**
* I have no idea how your relations are, but I hope
* you get the main idea with this. Just create what
* you should expect to have when you have this
* test case
*/
$connection = factory(Connection::class)->create([
'sender_id' => factory(Sender::class)->create()->id,
'receiver_id' => factory(Reciever::class)->create()->id,
'accepted' => false,
'connection_id' => factory(Connection::class)->create()->id,
]);
$response = $this->actingAs(factory(User::class)->create())
->post(
'/app/connection-request/accept',
[
'accept' => true,
'request_id' => $connection->id
]
);
$response->assertLocation('/')
->assertOk();
}
Then, you should not change anything except phpunit.xml environment variables pointing to your testing DB (locally) and it should work without you changing anything in your code.
tl;dr: can cypress variables be stored in some accesible place (like this or something similar) or do I have to get into an endless callback cycle if I want to access all of them for a single usage?
Long story:
the app I'm trying to cover with cypress tests runs on many different datasets so in order to prepare the test data before the test, I usually make few API calls and I'd like to work with their results:
Example:
The test should cover a "delete task" functionality. As test data, I want to create a task beforehand over our API. To do this, I need to make these calls:
Call ".../users/me" to get my userId (one of required params)
Call ".../users" to get a list of all users for particular dataset (first id is used to filter another, that is then used for assigneeId, another required param)
Call ".../tasks" with previous id's as required parameters
I currently have custom commands that handle those specific api calls while returning the result. I then call those commands and save their return as cypress variable. Should I want to do the third api call, I have to do something like this:
cy.getUserID().then((userId) => {
cy.wrap(userId).as('userId')
})
cy.getAllUsersForTenant().then((users) => {
cy.get('#userId').then((userId) => {
const result = users.find((escalationUserId) => escalationUserId !== userId)
cy.wrap(result.id).as('assigneeId')
})
})
cy.get('#assigneeId').then((assigneeId) => {
cy.get('#userId').then((userId) => {
// do the POST call with both assigneeId and userId available
})
})
Right now it's not really a big deal but I can imagine that I'l need more than 2 variables, will I have to add more nested callbacks or is there a way to store those variables at one accessible place?
I sort of figured it out by looking at other tickets - if I use function() instead of arrow functions, it is possible to share this context.
Is it possible to use cypress for testing relationship between models?
For example, I have this kind of relationship: a teacher has many students, each student belongs to a teacher. After the teacher A logged in, at url "/my-students", he or she will see a list of all his or her students.
What I want to test is to make sure none of the students listed on "/my-students" page belong to teacher B than teacher A.
Can I test this case with cypress? Is it possible and how to do it if it's possible?
The short answer is "yes" you can absolutely do this kind of testing. There are dozens of ways, but I'm going to suggest what I consider to be the simplest approach.
Make sure the data being used by your website doesn't change. You want your tests to be deterministic... none of this run a database query to determine the expected results stuff.
The first time through, verify the page contents manually
Use cy.snapshot() to record the current page state for future comparison. This is an additional npm package from Gleb Bahmutov (a Cypress developer). Full instructions, including installation, can be found here.
Your hypothetical test would look something like this:
describe('student directory page', () => {
beforeEach(() => {
// Log in
cy.logIn('lizstewart#example.com') // This is usually a custom command; up to you
})
it('displays the correct students', () => {
// Go to the page
cy.visit('my-students')
// Check for the correct students
cy.get('#studentList').snapshot()
})
})
The first time the test runs it will pass no matter what, and will write out a file titled snapshots.js, which you can commit to your repo. All subsequent test runs will fail if the HTML output doesn't match the previous content exactly.
It's a blunt approach, but it's quick and effective.
I have many test cases for the same application views. These views display different data depending on the response from the server. I have 18 test cases in which I check 6 almost identical views. I noticed that a lot of tests are repeating to me.
I have a question, can you store any references to avoid duplicate code?
In short, yes you can.
Cypress commands can be queued from an external function, so long as that function is called from somewhere inside an it() block. You can create a separate function that takes arguments to help it distinguish between your different views.
Here's a simple example:
function myExternalFunction(info) {
cy.log(info);
// ...
}
describe('My test', function() {
it('Calls another function to queue test commands', function() {
myExternalFunction("test");
}
}
Is there any way to create automated test case in laravel. It means when I will run the test case it will open browser automatically, check the validation, submit the form etc.
Yes, it is quite possible. From the documentation:
Laravel provides several methods for testing forms. The type, select,
check, attach, and press methods allow you to interact with all of
your form's inputs.
A sample test case would look something like:
public function testNewUserRegistration()
{
$this->visit('/register')
->type('Taylor', 'name')
->check('terms')
->press('Register')
->seePageIs('/dashboard');
}