I try to mock a method of my service with Mockery lib. It works if I call that method from the test's context. But if I call it from another method (for example, it calls from another tested method) - it returns original data from implementation, but not from mock. What I'm doing wrong?
The example is below.
I added contract's because of my real implementation uses it. I don't think the problem is related to interfaces.
app/Contracts/TransactionsServiceContract.php
namespace App\Contracts;
interface TransactionsServiceContract
{
public function getAllRequests(): array;
public function getRequests(array $necessaryFields): array;
}
app/Services/TransactionsService.php
namespace App\Services;
use App\Contracts\TransactionsServiceContract;
class TransactionsService implements TransactionsServiceContract
{
public function getAllRequests(): array
{
return [
'foo' => [
'metric' => 'foo',
],
'bar' => [
'metric' => 'bar',
],
'another' => [
'metric' => [
// Some fields
],
],
];
}
public function getRequests(array $necessaryFields): array
{
// dd($this->getAllRequests()); // -> for the test context it returns original value (above's one)
return collect($this->getAllRequests())->only($necessaryFields)
->map(function (array $metric) {
return $metric['formula'];
})
->toArray();
}
}
tests/Feature/TransactionsServiceTest.php
namespace Tests\Feature;
use App\Contracts\TransactionsServiceContract;
use Tests\TestCase;
class TransactionsServiceTest extends TestCase
{
/** #var TransactionsServiceContract */
private $_transactionsService;
public function setUp()
{
parent::setUp();
$requests = [
'test1' => [
'metric' => 'test 1',
],
'test2' => [
'metric' => 'test 2',
],
];
$this->_transactionsService = \Mockery::mock(app()->make(TransactionsServiceContract::class))->makePartial();
$this->_transactionsService->shouldReceive('getAllRequests')->andReturn($requests);
}
public function testInternalCall()
{
$directCall = $this->_transactionsService->getAllRequests(); // returns array "requests" from the setUp method
dump($directCall);
$internalCall = $this->_transactionsService->getRequests(['test1']);
dd($internalCall); // if we call getAllRequests into getRequests, but not from test's context, we get original array from real implementation, but not test's mock
}
}
Versions of libs/frameworks:
Laravel: v5.7.19
PHPUnit: 7.5.1
Mockery: 1.2.0
Thanks for attention. Happy new year! :)
When you call \Mockery::mock(app()->make(TransactionsServiceContract::class))->makePartial(); in your setUp method, you're not really replacing the implementation existing in the app container. Laravel's container provides you with the bind method, to do that (the documentation for that). Besides you wouldn't replace an interface with a mock, as interfaces don't do anything per definition.
So in fact you would do something like:
app()->bind('\App\TransactionsService', $mockedTransactionService);
Note this will only work if your code gets an instance of the TransactionService by injection or resolving, not by calling new TransactionService.
Related
I was performing unit test in my application, and some of the functions include closure functions like below:
<?php
namespace JobProgress\Transformers;
use League\Fractal\TransformerAbstract;
class CustomersTransformer extends TransformerAbstract {
public function includesCreatedBy($customer) {
$user = $customer->createdBy;
if($user) {
return $this->item($user, function($user){
\Log::info('unit test');
return [
'id' => (int)$user->id,
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'full_name' => $user->full_name,
'full_name_mobile' => $user->full_name_mobile,
'company_name' => $user->company_name,
];
});
}
}
}
Note : I have extended TransformerAbstract class of Fractal and using the item function as $this->item from the TransformerAbstract class.
Here is how i am executing my test:
public function testIncludeDeletedBy() {
$customer = factory(Customer::class)->create();
$include = $this->transformer->includesCreatedBy($customer);
$object = $include->getData()->first();
$this->assertInstanceOf(User::class, $object);
}
My unit test is not executing the code that i have written in closure function.
Like as i have mentioned in above code, i have added some logs but my unit test is not executing that portion of the code.
Can anyone please help me
This might be related to the implementation detail of the $this->item() method. You may expect the closure to be executed while as we can see, the closure only needs to be parsed and then passed to the method as a parameter.
The code shows nothing that it is actually executed and therefore you should not expect this to be as it is an implementation detail not under your test.
You could de-anonymize the closure and call it directly to be able to test it as a unit.
So you have one test for the if-clause in the existing method and one test which allows you to run (not only parse) the code you currently have as an anonymous function / closure.
This is sometimes called a test-point and can bring you up speed. The smaller the units you have are, the easier it is to test them.
<?php
namespace JobProgress\Transformers;
use League\Fractal\TransformerAbstract;
class CustomersTransformer extends TransformerAbstract {
public function includesCreatedBy($customer) {
$user = $customer->createdBy;
if($user) {
return $this->item($user, $this->transformImplementation(...));
}
}
public function transformImplementation(object $user) {
\Log::info('unit test');
return [
'id' => (int)$user->id,
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'full_name' => $user->full_name,
'full_name_mobile' => $user->full_name_mobile,
'company_name' => $user->company_name,
];
}
}
For the three dots (...), this is PHP 8.1 syntax, compare PHP RFC: First-class callable syntax for alternative PHP syntax before 8.1 for callables, e.g.:
Closure::fromCallable([$this, 'transformImplementation'])
I am using this ("browserstack/browserstack-local": "^1.1") package to run dusk tests on BrowserStack. Now the requirement is to run tests on multiple and different devices with different browsers. Currently, I am following this approach to run tests.
private function browserStackCaps($local_identifier)
{
return [
'project' => config('app.name'),
'browserstack.local' => 'true',
'browser' => env('BROWSER'),
'device' => env('DEVICE'),
'acceptSslCert' => true,
'resolution' => '1920x1080'
];
}
The drawback of this approach is I have to change the device name and browser name in the .env file every time I need to run tests on a different device/browser. Is there any way I can run tests on the provided array? The array that contains devices and browser information.
I know this is old, but I found this page while searching for a solution. I ended up building one myself that would probably meet your use-case. The biggest hurdle that I had was $this->browse() in a normal Dusk test was using a single instance of Laravel\Dusk\Browser and the new capabilities were not being pulled in. This implementation adds a function called performTest to the DuskTestCase.php file. This function loops through a set of capabilities and instantiates a new instance of Laravel\Dusk\Browser for each test. This function works similarly to the existing browse function in Laravel Dusk. You call performTest by passing it a callable that accepts a single parameter which is an instance of Laravel\Dusk\Browser
Dusk Test Case
<?php
namespace Tests;
use Laravel\Dusk\TestCase as BaseTestCase;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\Remote\DesiredCapabilities;
abstract class DuskTestCase extends BaseTestCase
{
use CreatesApplication;
protected array $capabilities;
private const BROWSERS = [
'ios_14_iphone_xs_safari' => [
"os_version" => "14",
"device" => "iPhone XS",
"real_mobile" => "true",
"browserstack.local" => "true",
'acceptSslCerts' => 'true'
],
'mac_osx_catalina_safari' => [
"os" => "OS X",
"os_version" => "Catalina",
"browser" => "Safari",
"browser_version" => "13.0",
"browserstack.local" => "true",
"browserstack.selenium_version" => "3.14.0",
"resolution" => "1920x1080",
'acceptSslCerts' => 'true',
]
];
/**
* Create the RemoteWebDriver instance.
*
* #return \Facebook\WebDriver\Remote\RemoteWebDriver
*/
protected function driver()
{
$browserStackConnectionUrl = config('browserstack.connection_url');
return RemoteWebDriver::create(
$browserStackConnectionUrl, $this->capabilities
);
}
protected function performTest(Callable $test){
foreach(self::BROWSERS as $browserName => $capabilitySet){
try {
$this->capabilities = $capabilitySet;
$browser = $this->newBrowser($this->driver());
$test($browser);
$browser->quit();
fprintf(STDOUT, "\e[0;32m√ {$browserName}\r\n");
}
catch(\Exception $exception){
fprintf(STDOUT, "\e[0;31mX {$browserName}\r\n");
throw $exception;
}
}
}
}
Example Test
<?php
namespace Tests\Browser;
use Tests\DuskTestCase;
use Laravel\Dusk\Browser;
class ExampleTest extends DuskTestCase
{
public function testExample()
{
$this->performTest(function(Browser $browser){
$browser->visit('/')
->assertDontSee('Foobar');
});
}
}
config/browserstack.php
<?php
return [
'connection_url' => env('BROWSERSTACK_CONNECTION_URL')
];
you can implement this at your end. Fetch the list of Browsers and Devices you want to execute your tests on using the REST API and use the same.
REST API to be used:
curl -u "username:password"
https://api.browserstack.com/automate/browsers.json
Read more on this here:
https://www.browserstack.com/docs/automate/api-reference/selenium/browser#get-browser-list
I'm about to ...
extend my App/Orm/MyModel.php with Http/Json/V1/MyModel.php so I can keep the $appends, $hides, toArray() neatly tucked away in a V1
namespace and prefix some routing for V1
probably do some custom resolvers for route model binding
And I'm thinking ... really? They haven't built this in... what am I missing here? There's gotta be a quick, turn-key for this. I'm interested in knowing how other people are doing this, so please chime in.
Try Resources instead of Models
Have a look at resources:
https://laravel.com/docs/5.7/eloquent-resources
And add your logic to resources so that you display different versions of a model depending on the API version. You can still make use of $appends and $hidden.
With this approach we return a Resource of a model rather than the model itself.
Here is an example of a UserResource for different API versions:
class UserResource extends JsonResource
{
private $apiVersion;
public function __construct($resource, int $apiVersion = 2) {
$this->apiVersion = $apiVersion; // OPTION 1: Pass API version in the constructor
parent::__construct($resource);
}
public function toArray($request): array
{
// OPTION 2: Get API version in the request (ideally header)
// $apiVersion = $request->header('x-api-version', 2);
/** #var User $user */
$user = $this->resource;
return [
'type' => 'user',
'id' => $user->id,
$this->mergeWhen($this->apiVersion < 2, [
'name' => "{$user->first_name} {$user->last_name}",
], [
'name' => [
'first' => $user->first_name,
'last' => $user->last_name
],
]),
'score' => $user->score,
];
}
}
The you can call:
$user = User::find(5);
return new UserResource($user);
If you need a different connection you can do:
$user = User::on('second_db_connection')->find(5);
So V1 API gets:
{
id: 5,
name: "John Smith",
score: 5
}
and V2 API gets:
{
id: 5,
name: {
first: "John",
last: "Smith",
},
score: 5
}
Now if later you wanted to rename score to points in your DB, and in V3 of your API you also wanted to change your JSON output, but maintain backwards compatibility you can do:
$this->mergeWhen($this->apiVersion < 3, [
'score' => $user->points,
], [
'points' => $user->points,
])
Prefix routes
You can easily prefix routes as mentioned here: https://laravel.com/docs/5.7/routing#route-group-prefixes
Route::prefix('v1')->group(function () {
Route::get('users', function () {
// ...
});
});
Explicit Route Model Binding
To do custom route model bindings have a look at: https://laravel.com/docs/5.7/routing#route-model-binding
e.g.
Route::bind('user', function ($value) {
return App\User::where('name', $value)->first() ?? abort(404); // your customer logic
});
In my ZF2 Application, I had several custom form Elements with an injected database Adapter. I put the configuration in the module.php file with a method, like this:
public function getFormElementConfig()
{
return array(
'factories' => [
'dksCodeElementSelect' => function($container)
{
$db = $container->get(AdapterInterface::class);
$elemnt = new Form\Element\Select\DksCodeElementSelect();
$elemnt->setDb($db);
return $elemnt;
},
)
}
How can I configure custom form elements within a zend-expressive application?
Form class calls elements via 'FormElementManger'. and FormElementManager reads 'form_elements' key from config. It's basicly a container (service manager) so you have to configure it same as container (factories, invokables, aliases etc). Also, you have to register Zend/Form module to container. I didn't try it but has to be ths way;
(ps: it's too late here, if it doesn't work let me know so i can put a working example.)
class MyModule\ConfigProvider {
public function __invoke()
{
return [
'dependencies' => $this->getDependencies(),
'templates' => $this->getTemplates(),
'form_elements => [
/* this is where you will put configuration */
'factories' => [
MyElement::class => MyElementFactory::class,
]
]
];
}
}
I'm hard to know what is the ideal place to use the **\Yii::$app->language = 'pt';**
I tried in main.php view, but only the menu got the translation. In the tutorial-i18N says:
You may set the application language at runtime to the language that
the user has chosen. This has to be done at a point before any output
is generated so that it affects all the output correctly. Therefor
just change the application property to the desired value
My intention is to store the desired language in a LANGUAGE field in the user profile (along with FULL_NAME, etc.).
In the code, I need to know the correct location and how to use the same.
EDIT
#Timothée Planchais, this way works:
class SiteController extends Controller
{
public function init()
{
parent::init();
if(!Yii::$app->user->isGuest) {
Yii::$app->language = Yii::$app->user->identity->profile->language;
}
}
But work only in SiteController
To set the application language, edit the file config/web.php :
$config = [
'id' => 'myapp',
'name' => My App',
'basePath' => dirname(__DIR__),
'bootstrap' => ['log'],
'language' => 'pt',//HERE
...
]
You can do all in a custom Controller class which should be extended by all your controllers. In the init() function :
namespace app\components;
class Controller extends yii\web\Controller
{
public function init()
{
parent::init();
if(!Yii::$app->user->isGuest) {
Yii::$app->user->getIdentity()->language = Yii::$app->language;
}
}
}
SiteController for example :
class SiteController extends app\components\Controller
{
...
}