I have written two tests: one that makes a post request to an endpoint and awaits for a specific response containing status and message; and another one making the exact same request, but instead of await a response, it verifies if the database has the data matching what I just sent. Both these test are feature tests, and so far I have no unit test in my application; that happens because I have tested endpoint only.
So my idea is the following: instead of making a call to an endpoint in my second test, I could directly test my service method that creates a new register to the database. Would this be a valid unit test?
Personally, I think it would be valid because I am isolating a specific method and testing if the code works, and not if the integration works, even though there's integration of my code with the DB (Eloquent), my service method is the closest testable thing to the DB I have in my system.
My two tests, in the order I specified above:
/** #test */
public function a_group_can_be_created()
{
$this->withoutExceptionHandling()->signIn();
$group_data = [
'name' => $this->faker->word(),
'status' => $this->faker->boolean(),
];
$modules = ['modules' => Modules::factory(1)->create()->pluck('id')];
$response = $this->post(route('cms.groups.store'), array_merge($group_data, $modules));
$response->assertSessionHas('response', cms_response(trans('cms.groups.success_create')));
}
/** #test */
public function creating_a_group_persists_its_data_to_the_database()
{
$this->withoutExceptionHandling()->signIn();
$group_data = [
'name' => $this->faker->word(),
'status' => $this->faker->boolean(),
];
$modules = ['modules' => Modules::factory(1)->create()->pluck('id')];
$this->post(route('cms.groups.store'), array_merge($group_data, $modules));
$this->assertDatabaseHas('groups', $group_data);
$this->assertDatabaseCount('modules', 2);
$this->assertDatabaseCount('group_modules', 2);
}
Unit test in laravel "do not boot your Laravel application and therefore are unable to access your application's database or other framework services"
With that said, you can't access any database with a facade or with your eloquent model
so if you change your feature testing to unit testing, it will fail.
Unit test will work well if you don't use any of laravel framework utilities. I occasionally use it for testing a little self made library
But if you want to isolate it and create feature testing without calling the API endpoint, it will works too. It's really up to you to decide whether it is necessary to do that or not. But keep in mind that unnecessary test will make the test longer, especially if you use RefreshDatabase or DatabaseMigration trait. It will quite annoying to wait for them to finish
Related
The bounty expires in 5 days. Answers to this question are eligible for a +50 reputation bounty.
Chris Rockwell wants to draw more attention to this question.
I am trying to get comfortable with tests in Laravel and playing around with Dusk.
Given I have the following controller:
class CoursesController extends Controller {
private ApiServiceProvider $api;
public function __construct(ApiServiceProvider $apiServiceProvider) {
$this->api = $apiServiceProvider;
}
public function getCoursesCache(array $cIds = []) : array {
// Breakpoint here - always gets hit when running tests
if (empty($cIds)) {
$cIds = Request::capture()->query('cIds');
$cIds = explode(',', $cIds);
}
return $this->api->getCoursesCache($cIds);
}
}
Which is used by a route:
Route::get('/api/v1/courses/cache', 'App\Http\Controllers\Api\CoursesController#getCoursesCache')->name('courses.cache');
This route is used internally by a VueJS component, which is ultimately what I'd like to test.
I am using Dusk to do some browser based testing and I want to mock the controller response for getCoursesCache. However, when I use the following (with a breakpoint in the controller method) I always enter the controller instead of just returning the mock.
$courseController = $this->mock(CoursesController::class)->makePartial();
$item = new CourseCacheItem();
$item->name = $course->name;
$courseController->shouldReceive('getCoursesCache')
->with([$course->getKey()])
->andReturn([$item]);
$this->browse(function (Browser $browser) use ($course) {
$browser->visit('/')
->waitFor('.course-card-container--data-loaded', 10)
->screenshot('filename')
->assertSee($course->name);
});
I've also tried this to create the mock:
$cc = $this->createMock(CoursesController::class);
$item = new CourseCacheItem();
$item->name = $course->name;
$cc->expects($this->once())->method('getCoursesCache')->with([$course->getKey()])->willReturn([$item]);
Edit: I've also now tried Mocking and Spying on the injected service ApiServiceProvider but the code enters that real class during the test run as well.
My expectation is that my breakpoint within the actual CoursesController would never be hit - what am I doing wrong?
Your issue here is that you are dealing with two separate Laravel Runtime.
You can check this issue for more infos.
Basically, when you are making a Dusk test with an Http call, Dusk will make a real Http call to a fresh Laravel instance/runtime (using Chrome Headless).
So you end up having one runtime where you are lunching the test, and the second one where dusk is making an http call.
The second Laravel instance is not running your Mock and doesnt know about it. That's why you end up in the actual Controller.
One solution i found in the past is making a route responsible to mock what i need.
This route should be called in the the html page before the assertion is happening.
So when you make a Dusk http call some Js (in the second Laravel instance) will call the route and the mock will be setup.
Fortunately for you there is great package that can handle this for you. https://github.com/NoelDeMartin/laravel-dusk-mocking
I'm trying to use Laravel's app service container to resolve out mocked instances for testing. I've created a mock which works when making an instance of CS_REST_Subscribers alone, however if I provide arguments to the service container my mock no longer applies.
$this->mock(\CS_REST_Subscribers::class, function (MockInterface $mockery) {
$mockery
->shouldReceive('add')
->once();
});
get_class(app()->make(\CS_REST_Subscribers::class)); // returns Mockery_2_CS_REST_Subscribers
get_class(app()->make(\CS_REST_Subscribers::class, [
'list_id' => 'testing',
'auth_details' => ['api_token' => '123']
])); // returns CS_REST_Subscribers
Dump 1 gives me Mockery_2_CS_REST_Subscribers but dump 2 gives me CS_REST_Subscribers.
Any idea how to apply the mock even when passed constructor arguments? I can't help but feel like I'm missing something here...
I've just found the solution off the back of a Laravel raised issue https://github.com/laravel/framework/issues/19450#issuecomment-451549582
It seems that when passing parameters, Laravel's built in mocking bypasses building a mock instance.
The solution was to create my Mockery mock and then bind it to the service container directly, thus forcing Laravel to resolve what it has been given in the service container.
$mock = \Mockery::mock(\CS_REST_Subscribers::class)->makePartial();
$mock->shouldReceive('add')->once();
$this->app->bind(\CS_REST_Subscribers::class, fn() => $mock);
Looking deeper into this, I'm not sure it's even possible, which is a shame because I'm trying to learn TDD.
I would like to test my model with Billable being created and subscribed to a plan.
/** #test */
public function an_account_can_subscribe_to_a_plan() {
$account = factory("App\Account")->create();
Stripe::setApiKey('key');
$paymentMethod = PaymentMethod::create([
'type' => 'card',
'card' => [
'number' => '4242424242424242',
'exp_month' => '2',
'exp_year' => '2021',
'cvc' => '314'
]
]);
$subscription = $account->newSubscription('default', 'starter')->create($paymentMethod);
$this->assertTrue( $subscription->valid() );
}
The Laravel Cashier docs show how to send a token via Stripe.js, but that doesn't work for Unit Testing.
I've tried to include the Stripe library directly and create a PaymentMethod object, but this also requires me set an API key manually. Now the error I'm getting is that I have to verify my phone number to send raw credit card numbers to the stripe API.
I'm hoping there's a better way. How can I use Laravel Cashier in a TDD way and mock up fake subscriptions with fake payment methods?
Stripe provides not only test card numbers, but tokens and payment method's.
https://stripe.com/docs/testing#cards
Click the PaymentMethods tab. The value (for example pm_card_visa) can be used directly on the server-side in your tests without the need of front-end payment-intent implementation.
This is an example of a feature test I have:
/**
* #test
*/
public function a_user_can_subscribe_to_a_paid_plan()
{
$this->actingAs($this->user);
$this->setUpBilling();
$enterprise = Plan::where('name', 'Enterprise')->first()->stripe_plan_id;
$response = $this->post(route('paywall.payment'), [
'payment_method' => 'pm_card_visa',
'stripe_plan_id' => $enterprise
])
->assertSessionDoesntHaveErrors()
->assertRedirect();
}
Your tests may vary, but you can make a normal request to your billing controller with these test payment methods and it goes through just as it would if you do it on the front-end.
You may want to use stripe-mock for mocking during tests. If that doesn't fit your needs, mocking the objects directly may be a better option.
On this slide :
Sandiz Metz talks about how we should test from the outside without knowing anything about what goes inside the SUT.
I think her talk is focused on the unit tests, not so much about integration tests, but that still made me wonder...could using the factories in Laravel be an anti-pattern ?
It seems to me that using these factories mean I know how the database data has to be for the SUT to complete its task.
For example, say a user has to be able to edit his profile. With factories, I could do this :
/** #test */
public function the_user_can_update_his_profile()
{
$user = factory(User::class)->create();
// ACT
// ASSERT
}
But that knowledge seems to be far too deep and detailed. I have to know how to create a properly registered user. Following the idea of staying in the outside world, shouldn't I instead use an object that already exist to prepare the data for my tests ?
/** #test */
public function the_user_can_update_his_profile()
{
$userRepository = app(UserRepository::class);
$user = $userRepository->register('email#email.com', 'password123');
// ACT
// ASSERT
}
Going even further, how do I know this is the right way to do it ? Shouldn't I simply call the route that a user will use to register ?
/** #test */
public function the_user_can_update_his_profile()
{
$response = $this->json('POST', route('register_user'), ['email' => 'email#email.com', 'password' => 'password123']);
$userRepository = app(UserRepository::class);
$user = $userRepository->find($response['userId']);
// ACT
// ASSERT
}
But that (extreme) solution could also do tons of unnecessary other things (e.g. sending a confirmation email). It also needs the register route to work.
What did you experience as being the cleanest solution for a complicated project ?
As stated in the comments, you're mistaking the purpose and scope of a unit test and an integration test, and from this comes the confusion. Let's start with your "test":
/** #test */
public function the_user_can_update_his_profile()
{
$response = $this->json('POST', route('register_user'), ['email' => 'email#email.com', 'password' => 'password123']);
$userRepository = app(UserRepository::class);
$user = $userRepository->find($response['userId']);
// ACT
// ASSERT
}
This is an integration test. You are testing a "remote" route (okay, it's internal, but you're still testing every single component along the way). An integration test is perfect to confirm that business logic was implemented properly and that the route behaves as expected; it goes through its path from start to completion, testing every interaction between components and the logic in the controller+view itself.
This is all fine and good, but it doesn't really help us that much. We're still blind regarding testability of components themselves. This is where unit testing comes in.
Assume you have a class as follows:
<?php
class Foo {
public $value = 0;
public function __construct($value) {
$this->value = (int)$value;
}
public function getRemainder(int $item) {
return $this->value % $item;
}
}
You may very well end up using this class in one of your controllers; the point of unit testing this is to assert that:
the constructor does store the right value (as an integer)
the isModulo method does what it says on the tin.
To do so, we might write the following test:
public function isActualModuloClass() {
$modulo = new Foo(5);
$modulo_float = new Foo(2.3);
assert($modulo->value == 5, "Integer modulo constructor works");
assert($modulo_float->value == 2, "Float modulo casts to integer properly");
assert($modulo->getRemainder(5) == 0, "Modulo 5%5 is 0");
assert($modulo->getRemainder(4) == 1, "Modulo 5%4 is 1");
}
And sure enough, we've tested every single method, every single branch of our component in isolation. This is a unit test.
It gets messier with classes that interact with other objects, but when structured properly, it is very easy to leverage tools to inject mock copies of objects in order to be able to stub out interactions. When your code touches multiple things at once in an unclean fashion, your tests tend to end up a being gargantuan mess.
The talk you linked is about these tests, and ways to make them not be the fragile mess they tend to be in badly thought-out codebases. In theory, the author is right as well - you start from objects without dependencies, test those, then gradually go up assuming the contracts being established by those are sane and tested, and you work your way up the pyramid. In practice, it's very hard to do when the code is written with testing as an afterthought.
How can we mock Stripe in Laravel Unit Tests without using any external package like stripe-mock etc?
The job is to test the Controller feature where the secret is hardcoded and due to which test is failing.
Hey i ran into the same problem,
i am using aspectmock from codeception. Gave me some grief setting it up but im now able to mock all the responses with a json response. this way the json data goes thru the stripe classes and it throws the correct errors and returns the same objects.
hope that helps
https://github.com/Codeception/AspectMock
public function testAll()
{
$customerClass = new StripeCustomers();
test::double('Stripe\HttpClient\CurlClient', ['request' => [json_encode($this->allJsonData), 200, []]]);
$customer = $customerClass->all();
$this->assertArrayHasKey('data', $customer);
}