I want to make test cases, with phpunit and cakephp 3.x, shell that send email. This is my function into shell:
class CompaniesShellTest extends TestCase
{
public function monthlySubscription()
{
/* .... */
$email = new Email('staff');
try {
$email->template('Companies.alert_renew_success', 'base')
->theme('Backend')
->emailFormat('html')
->profile(['ElasticMail' => ['channel' => ['alert_renew_success']]])
->to($user->username)
//->to('dario#example.com')
->subject('Eseguito rinnovo mensile abbonamento')
->viewVars(['company' => $company, 'user' => $user])
->send();
} catch (Exception $e) {
debug($e);
}
/* ... */
}
}
In my testing class i have this functions
/**
* setUp method
*
* #return void
*/
public function setUp()
{
parent::setUp();
$this->io = $this->getMockBuilder('Cake\Console\ConsoleIo')->getMock();
$this->CompaniesShell = new CompaniesShell($this->io);
}
/**
* tearDown method
*
* #return void
*/
public function tearDown()
{
unset($this->CompaniesShell);
parent::tearDown();
}
/**
* Test monthlySubscription method
*
* #return void
*/
public function testMonthlySubscription()
{
$email = $this->getMock('Cake\Mailer\Email', array('subject', 'from', 'to', 'send'));
$email->expects($this->exactly(3))->method('send')->will($this->returnValue(true));
$this->CompaniesShell->MonthlySubscription();
}
But this doesn't work.
Any ideas? I want to check if the mails are successfully sent and how many times.
The way you wrote your code won't work.
$email = new Email('staff');
And:
$email = $this->getMock('Cake\Mailer\Email', array('subject', 'from', 'to', 'send'));
How do you expect the class you call to magically replace the $email variable with your mock object? You'll need to refactor your code.
This is how I would do it:
First implement a custom mailer like SubscriptionMailer. Put your mail code into this mailer class. That makes sure you have nice separated and reuseable code.
public function getMailer() {
return new SubscriptionMailer();
}
In your test mock the getMailer() method of your shell and return your email mock.
$mockShell->expects($this->any())
->method('getMailer')
->will($this->returnValue($mailerMock));
You can then do the expectation you already have.
$email->expects($this->exactly(3))->method('send')->will($this->returnValue(true));
Also depending on what your shell method is doing, maybe it is better to send the email in the afterSave callback (again using the custom mailer class) of a model object (table) that is processing the data from your shell. Check the example at the end of this page.
Related
What I want to do, is to assert that a class's method was called during a request. The code will probably explain better what I'm trying to do:
Test:
public function use_default_provider_when_getting_addresses()
{
$spy = $this->spy(CraftyClicksService::class);
$this->app->bind(CraftyClicksService::class, function () use ($spy) {
return $spy;
});
$addresses = $this->get('/api/v1/address?postcode=POSTCODE')->decodeResponseJson();
$this->assertTrue(in_array([
'line_1' => 'Line 1',
'line_2' => 'Line 2',
'postcode' => 'POSTCODE',
], $addresses));
$spy->shouldHaveReceived('getAddresses')->once();
}
The request hits a simple controller:
public function show(Request $request, AddressService $addressService)
{
return $addressService->findAddress($request->input('postcode'));
}
The address service class (for now) then calls another class's function to retrieve the addresses.
class AddressService
{
protected CraftyClicksService $craftyClicksService;
/**
* AddressService constructor.
*/
public function __construct()
{
$this->craftyClicksService = new CraftyClicksService();
}
/**
* #param string $postcode
* #return array
*/
public function findAddress(string $postcode)
{
return $this->craftyClicksService->getAddresses($postcode);
}
I can 'spy' on the AddressService class and assert it received the findAddress method, however it seems I cant assert the getAddresses function for the CraftyClicksService class.
I always get a Method getAddresses(<Any Arguments>) from Mockery_3_App_Services_CraftyClicksService should be called at least 1 times but called 0 times. error, even though the response test passes and I can confirm that method is indeed the one called.
Tried to use the $this->app->bind as well as the $this->spy() but test still fails.
You need to use dependency injection and let the service container resolve the dependency from the container so the AddressService class should change to
class AddressService
{
protected CraftyClicksService $craftyClicksService;
/**
* AddressService constructor.
*/
public function __construct(CraftyClicksService $CraftyClicksService)
{
$this->craftyClicksService = $CraftyClicksService;
}
/**
* #param string $postcode
* #return array
*/
public function findAddress(string $postcode)
{
return $this->craftyClicksService->getAddresses($postcode);
}
This way, the spy object which you have bound it to the container will be used.
Here is my case
I have cron job(console command)
/**
* Execute the console command.
*
* #return mixed
*/
public function handle(OrdersMagentoService $magentoService)
{
try {
$orders = $magentoService->getRemoteOrders();
print_r($orders);
} catch (SoapFault $e) {
$this->line('Error connect to soap api, error: ' . $e->getMessage());
die;
} catch (\Throwable | \Exception $e) {
print_r($e->getMessage());
die;
}
}
In handle method i automaticaly inject OrdersMagentoService, this service do connect to magento soap api and exctend from BaseMagentoService
class OrdersMagentoService extends BaseMagentoService
{
public function getRemoteOrders()
{
$complex = [
'complex_filter' => [
[
'key' => 'status',
'value' =>
[
'key' => 'in',
'value' => 'closed,canceled,holded,processing,complete'
]
],
[
'key' => 'updated_at',
'value' => [
'key' => 'from',
'value' => now()->subDays(200),
]
]
]
];
return $this->salesOrderList($complex);
}
}
class BaseMagentoService
{
/**
* #var
*/
private $client;
/**
* #var
*/
private $session;
/**
* #var \Illuminate\Config\Repository|mixed
*/
protected $config;
/**
* BaseMagentoService constructor.
*/
public function __construct()
{
$this->config = config('services.magento');
$this->connect();
}
/**
* Do connect to soap api v2
*
* #throws \SoapFault
*/
private function connect()
{
$this->client = new SoapClient($this->getApiUrl());
$this->session = $this->client->login($this->config['user_name'], $this->config['password']);
}
public function __call($resource, $arguments)
{
return $this->client->$resource($this->session, ...$arguments);
}
}
In BaseMagentoService constructor i create connection to magento soap api. But if connection throw error(for example wrong username and pass) then i can't handle this in cron job file. I understand that laravel at first create OrdersMagentoService, it throw error and try catch in handle function not works, but i dont know how to fix this.
I can add in handle method
try {
$magentoService = resolve(OrdersMagentoService::class)
$orders = $magentoService->getRemoteOrders();
print_r($orders);
} catch (SoapFault $e) {
$this->line('Error connect to soap api, error: ' . $e->getMessage());
die;
} catch (\Throwable | \Exception $e) {
print_r($e->getMessage());
die;
}
And remove automatic DI, and tit should work well, but i dont want to do it.
Also if i add some try catch in connect method of BaseMagentoService then i cant log this error in my cron job.
What is best way to handle this?
First I would switch the handling from the artisan command to a Job or an Event that might be more suited to the problem. Then your artisan command simply fires off this new job or event each time is executed.
This is also somehow stated in the docs as a best practice:
For greater code reuse, it is good practice to keep your console commands light and let them defer to application services to accomplish their tasks.
Then if you look at the documentation about jobs, there is a section about error handling for failed jobs where you are told that you can define a method that gets triggered once he job has failed. Example excerpt taken from the docs:
class YourJob implements ShouldQueue
{
/**
* Execute the job.
*
* #param AudioProcessor $processor
* #return void
*/
public function handle(AudioProcessor $processor)
{
// Process uploaded podcast...
}
/**
* The job failed to process.
*
* #param Exception $exception
* #return void
*/
public function failed(Exception $exception)
{
// Send user notification of failure, etc...
}
}
You can read more in the documentation. There is also a paragraph about global jobs failure handling that might suit your use case
How one would test email is sent as a final outcome after triggering a notification or doing an action that triggers notification?
Ideally, there is a notification merely for sending an email. My first thought was to trigger it and then check if Mail::assertSent() is sent. However, it appears that this does not work as Notification returns Mailable but does not invoke Mail::send().
Relevant GitHub issue: https://github.com/laravel/framework/issues/27848
My first approach for test:
/** #test */
public function notification_should_send_email()
{
Mail::fake();
Mail::assertNothingSent();
// trigger notification
Notification::route('mail', 'email#example.com')
->notify(new SendEmailNotification());
Mail::assertSent(FakeMailable::class);
}
while the Notification toMail() method looks as:
/**
* Get the mail representation of the notification.
*
* #param mixed $notifiable
* #return \Illuminate\Notifications\Messages\FakeMailable
*/
public function toMail($notifiable)
{
return (new FakeMailable())
->to($notifiable->routes['mail']);
}
The set-up example is available https://github.com/flexchar/laravel_mail_testing_issue
You can use mailCatcher then extends your TestCase
class MailCatcherTestCase extends TestCase
{
protected $mailCatcher;
/**
* MailCatcherTestCase constructor.
* #param $mailCatcher
*/
public function __construct($name = null, array $data = [], $dataName = ''
) {
parent::__construct($name, $data, $dataName);
$this->mailCatcher = new Client(['base_uri' => "http://127.0.0.1:1080"]);
}
protected function removeAllEmails() {
$this->mailCatcher->delete('/messages');
}
protected function getLastEmail() {
$emails = $this->getAllEmail();
$emails[count($emails) - 1];
$emailId = $emails[count($emails) - 1]['id'];
return $this->mailCatcher->get("/messages/{$emailId}.json");
}
protected function assertEmailWasSentTo($recipient, $email) {
$recipients = json_decode(((string)$email->getBody()),
true)['recipients'];
$this->assertContains("<{$recipient}>", $recipients);
}
}
then you can use in you test
/** #test */
public function notification_should_send_email()
{
// trigger notification
Notification::route('mail', 'email#example.com')
->notify(new SendEmailNotification());
$email = $this->getLastEmail();
$this->assertEmailWasSentTo($email, 'email#example.com');
}
since you can fetch the mail, so that you can test mail body, subject, cc, attachment etc.
don't forget to remove all mails in tearDown
hope this helps.
I have a receiveEmail boolean field in the User model of a Laravel application. How do I ensure that mail notifications respect this field, and only sends the email to the user if the field is true?
What I want is that this code:
$event = new SomeEvent($somedata);
Auth::user()->notify($event);
where SomeEvent is a class that extends Notification and implements 'mail' on the via() method, only sends an email if the user has allowed emails.
Have any one checked this via() method:
https://laravel.com/docs/6.x/notifications#specifying-delivery-channels
public function via($notifiable)
{
// $notifiable object is User instance for most cases
$wantsEmail = $notifiable->settings['wants_email']; // your own logic
if(!$wantsEmail){
// no email only database log
return ['database'];
}
return ['database', 'mail'];
}
I hope this will work while sending notifications to multiple users too. Thanks
I ended up creating a new Channel that implements the checking. In app/channels, add your channel, something like this:
namespace App\Channels;
use App\User;
use Illuminate\Notifications\Channels\MailChannel;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Arr;
class UserCheckMailChannel extends MailChannel
{
/**
* Send the given notification.
*
* #param mixed $notifiable
* #param \Illuminate\Notifications\Notification $notification
* #return void
*/
public function send($notifiable, Notification $notification)
{
// check if user should receive emails. Do whatever check you need here.
if ($notifiable instanceof User && !$notifiable->receiveEmails) {
return;
}
// yes, convert to mail and send it
$message = $notification->toMail($notifiable);
if (!$message) {
return;
}
parent::send($notifiable, $notification);
}
}
Then bind your class on Providers/AppServiceProvider.php to the old mail class:
/**
* Register any application services.
*
* #return void
*/
public function register()
$this->app->bind(
\Illuminate\Notifications\Channels\MailChannel::class,
UserCheckMailChannel::class
);
}
try to create new method in user model like this..
user model file..
public function scopeNotifyMail() {
if($this->receiveEmail == true) { //if field is enable email other wise not send..
$event = new SomeEvent($somedata);
$this->notify($event);
}
}
and now call like this in controller..
Auth::user()->notifyMail();
or
App\User::find(1)->notifyMail();
or
App\User::where('id',1)->first()->notifyMail();
I am trying to do pushnotification when a new user signup.So I created events called MemberNotificationEvents, when I fired an event event(new MemberNotificationEvent($UserDetails)); on my signUpController flow is completely going but on the MemberNotificationListener a public function handle(MemberNotificationEvent $event) return error that :
Call to a member function send() on string
I put full code of MemberNotificationListener :
<?php
namespace App\Listeners;
use App\Events\MemberNotificationEvent;
use App\Services\PushNotificationService;
use Illuminate\Contracts\Queue\ShouldQueue;
class MemberNotificationListener implements ShouldQueue
{
private $pushNotificationService;
/**
* Create the event listener.
*
* #return void
*/
public function __construct()
{
$this->pushNotificationService = PushNotificationService::class;
}
private function getMessageBody($username)
{
return "Awesome! Welcome " . $username . " to IDM";
}
/**
* Handle the event.
*
* #param object $event
* #return void
*/
public function handle(MemberNotificationEvent $event)
{
$username = $event->UserDetails->name;
$message = $this->getMessageBody($username);
$this->pushNotificationService->send($event,['body' => $message]); // throw error
}
}
What is the problem in my code?
The problem is with this line:
$this->pushNotificationService = PushNotificationService::class;
When you do SomeClass::class, it means you supply the class name - not the actual class.
Hence, when you later do $this->pushNotificationService->send(...), the push notification service is just the class name and not the service class.
The second part of the problem is that you need an actual object to put in there. Laravel can inject it for you in the constructor, and then you can supply it. Like this:
public function __construct(PushNotificationService $service)
{
$this->pushNotificationService = $service;
}