I am working on Laravel 5.7 and i'd like to ask you a question regarding the PHPUnit testing.
I have a test class, let's say ProductControllerTest.php, with two methods testProductSoftDelete() and testProductPermanentlyDelete(). I want to use the annotation #depends in the testProductPermanentlyDelete() in order to soft-delete first a product and then get the product id and proceed to permanently deletion test. The problem here is that the DatabaseTransaction trait runs the transactions in every test (method) execution. I need to start the transaction before all the tests of my ProductControllerTest class and then rollback the transaction at the end of all tests. Do you have any ideas? From what i have searched from the web nothing worked properly.
public function testProductSoftDelete()
{
some code
return $product_id;
}
/**
* #depends testProductSoftDelete
*/
public function testProductPermanentlyDelete($product_id)
{
code to test permanently deletion of the product with id $product_id.
There is a business logic behind that needs to soft delete first a
product before you permanently delete it.
}
Does the following make sense?
namespace Tests\App\Controllers\Product;
use Tests\DatabaseTestCase;
use Tests\TestRequestsTrait;
/**
* #group Coverage
* #group App.Controllers
* #group App.Controllers.Product
*
* Class ProductControllerTest
*
* #package Tests\App\Controllers\Product
*/
class ProductControllerTest extends DatabaseTestCase
{
use TestRequestsTrait;
public function testSoftDelete()
{
$response = $this->doProductSoftDelete('9171448');
$response
->assertStatus(200)
->assertSeeText('Product sof-deleted successfully');
}
public function testUnlink()
{
$this->doProductSoftDelete('9171448');
$response = $this->actingAsSuperAdmin()->delete('/pages/admin/management/product/unlink/9171448');
$response
->assertStatus(200)
->assertSeeText('Product unlinked successfully');
}
}
namespace Tests;
trait TestRequestsTrait
{
/**
* Returns the response
*
* #param $product_id
* #return \Illuminate\Foundation\Testing\TestResponse
*/
protected function doProductSoftDelete($product_id)
{
$response = $this->actingAsSuperAdmin()->delete('/pages/admin/management/product/soft-delete/'.$product_id);
return $response;
}
}
namespace Tests;
use Illuminate\Foundation\Testing\DatabaseTransactions;
abstract class DatabaseTestCase extends TestCase
{
use CreatesApplication;
use DatabaseTransactions;
}
Create a separate function to execute the same behavior twice:
public function testProductSoftDelete()
{
doSoftDelete();
}
public function testProductPermanentlyDelete()
{
doSoftDelete();
doPermanentDelete();
}
Your case isn't a case of test dependency, but what you really want to do is to check if a soft deleted can be permanent deleted (or something like that).
Creating a dependency, in this case, will increase complexity of the test.
It's usually better to mount the test scenario from scratch (data/objects), then execute the logic, and then test if the actual scenario is the expected.
Related
My idea is to specify in each test class parameters, depends how we want to wrap test in transactions. I use Laravel 5.1.
use DB;
class TestCase
{
protected static $wrapMethodInTransaction = true;
protected static $wrapClassInTransaction = false;
/**
* #beforeClass
*/
public static function isClassWrappedInTransactionBefore()
{
if (!static::$wrapMethodInTransaction && static::$wrapClassInTransaction) {
DB::beginTransaction();
}
}
/**
* #afterClass
*/
public static function isClassWrappedInTransactionAfter()
{
if (!static::$wrapMethodInTransaction && static::$wrapClassInTransaction) {
DB::rollback();
}
}
}
The case is that #beforeClass & #afterClass fires when application is not booted yet/already dead, thus class DB is not accesible. I just want to run those methods once per class, but when application is booted/not killed yet. Are there any solutions for this?
DatabaseTransactions trait does not do the work, because it starts transaction after setUp() and rollbacks it after tearDown(). Laravel's 'magic'. I want the setUp() to be in transaction.
I have read so many examples and cannot see what I am doing wrong, please if someone could help.
I am getting an error when running tests (error at the bottom of post), that doens't happen when viewing the page in the browser. I think this is because the repository isn't being instantiated properly so the relevant method not fired? Or some issue with the API call in the mock.
Controller:
namespace ShopApp\Http\Controllers\StoreFront;
use Illuminate\Http\Request;
use ShopApp\Http\Requests;
use ShopApp\Http\Controllers\Controller;
use ShopApp\Repositories\Contracts\CategoryRepositoryContract;
use ShopApp\Repositories\Contracts\PublicationRepositoryContract;
class PagesController extends Controller
{
private $publication;
private $category;
public function __construct(PublicationRepositoryContract $publication, CategoryRepositoryContract $category){
$this->publication = $publication;
$this->category = $category;
}
/**
* Homepage.
* #return view
* #internal param PublicationRepositoryContract $publication
* #internal param CategoryRepositoryContract $category
*/
public function home()
{
$mostRecent = $this->publication->getRecent();
return view('pages/home')->with(compact('mostRecent'));
}
}
Publication Repository:
<?php
namespace ShopApp\Repositories;
use ShopApp\Models\API\APIModel;
use GuzzleHttp\Client as GuzzleClient;
use Illuminate\Support\Facades\Config;
use ShopApp\Repositories\Contracts\PublicationRepositoryContract;
class localPublicationRepository extends APIModel implements PublicationRepositoryContract
{
private $end_point; // where are we talking to?
public $response; //what did we get back?
public function __construct(GuzzleClient $client){
parent::__construct(new $client(['base_uri' => Config::get('customerprovider.local.api.base_uri'), 'http_errors' => true]));
$this->end_point = 'Publications';
}
/**
* Get all publications
*/
public function getAll(){
$this->response = $this->get($this->end_point);
$publications_with_slugs = $this->assembleSlugs($this->response);
return $publications_with_slugs;
}
/**
* Get recent publications
*/
public function getRecent(){
return $this->getAll(); //#todo - update this to just get the most recent
}
}
Test:
<?php
namespace Tests\Unit\Controllers;
use Tests\TestCase;
use Mockery as m;
class PagesControllerTest extends TestCase
{
public $publicationRepositoryContract;
/**
* Setup mocks etc
*/
public function setUp()
{
parent::setup();
$this->publicationRepositoryContract = m::mock('ShopApp\Repositories\Contracts\PublicationRepositoryContract');
}
/**
* Teardown mocks
*/
public function tearDown()
{
m::close();
parent::tearDown();
}
/**
* A basic test example.
*
* #return void
*/
public function testHomepage()
{
$this->publicationRepositoryContract
->shouldReceive('getRecent')
->once();
$this->app->instance('ShopApp\Repositories\Contracts\PublicationRepositoryContract', $this->publicationRepositoryContract);
$response = $this->call('GET', '/');
$response->assertStatus(200);
// getData() returns all vars attached to the response.
$mostRecent = $response->original->getData()['mostRecent'];
$response->assertViewHas('mostRecent');
$this->assertInstanceOf('Array', $mostRecent);
}
}
Test Error:
Expected status code 200 but received 500.
Failed asserting that false is true.
/home/vagrant/Code/imsnews-site/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:61
/home/vagrant/Code/imsnews-site/tests/Unit/Controllers/PagesControllerTest.php:53
Contents of Response ($response->Content()):
<span class="exception_title"><abbr title="ErrorException">ErrorException</abbr> in <a title="/home/vagrant/Code/imsnews-site/storage/framework/views/229655ca372490c9c0b1f5e7e2d4e91e6d3bbf6c.php line 262">229655ca372490c9c0b1f5e7e2d4e91e6d3bbf6c.php line 262</a>:</span>\n
<span class="exception_message">Invalid argument supplied for foreach() (View: /home/vagrant/Code/imsnews-site/resources/views/pages/home.blade.php)</span>\n
Line 262 from home.blade.php:
#foreach ($mostRecent as $key => $publication)
It seems clear that the method ->getRecent(), which in turn, calls ->getAll() on the publications repository is not returning an array as it should, but I don't know why.
Blade isn't complaining about the variable mostRecent not existing, it's complaining about it being invalid in a foreach.
Could this have something to do with Guzzle and the fact it's calling my API from the mocked test object?
Please help, hours have been lost..
Thanks.
Try mocking the concrete repository, and swap it out for the contract in the container. It seems you are mocking the contract, and then swapping it out for the same contract in your container.
TL;DR :
The key was you HAVE to have ->andReturn([]); on the test, like so:
$this->publicationRepositoryContract
->shouldReceive('getRecent')
->once()->andReturn([]);
My test only had:
$this->publicationRepositoryContract
->shouldReceive('getRecent')
->once();
Thanks to Ayo for pointing this out. It only became clear after deleting other parts of my test.
Is it possible to have Cest files that extend a parent class and make use of a common test "login" that other tests depend on using the #depends
So my Cest file looks similar to the one found in this post which explains how to login and re-use the cookie in another test - https://stackoverflow.com/a/25547268/682754
I've got this common class but the test in the child class doesn't run and outputs...This test depends on 'commonCest::login' to pass.
<?php
class commonCest {
const COOKIE_NAME = 'PHPSESSID';
protected $cookie;
/**
* Most tests will need to depend on this function as it logs the user in and stores the session cookie, use the
* "#depends" phpdoc comment
*
* The cookie in then re-used by calling the following in tests:
*
* $I->setCookie(self::COOKIE_NAME, $this->cookie);
*
* #param \AcceptanceTester $I
*/
public function login(AcceptanceTester $I) {
$I->wantTo('Login');
$I->amOnPage('/login');
$I->fillField(array('name' => 'username'), 'aaaaaa');
$I->fillField(array('name' => 'password'), 'abcdef');
$I->click('.form-actions button');
$I->seeElement('.username');
$this->cookie = $I->grabCookie(self::COOKIE_NAME);
}
}
<?php
use \AcceptanceTester;
class rolesCest extends commonCest
{
/**
* #depends login (This doesn't work)
* #depends commonCest:login (This doesn't work)
* #depends commonCest::login (This doesn't work)
*/
public function listUsers(AcceptanceTester $I)
{
// tests
}
?>
Simply don't have commonCest under your cest directory. Login will then be run for every case of a class extending commonCest in your cest directory as it exists in all of them. You however shouldn't use #depends for this. Rather login should be in your actor or a helper, and that should be called from _before in your parent class.
Or just use stepobjects https://codeception.com/docs/06-ReusingTestCode#stepobjects and call your needed functions from _before
I appear to be having issues with my spec tests when it comes to stubs that are calling other methods.
I've been following Laracasts 'hexagonal' approach for my controller to ensure it is only responsible for the HTTP layer.
Controller
<?php
use Apes\Utilities\Connect;
use \OAuth;
class FacebookConnectController extends \BaseController {
/**
* #var $connect
*/
protected $connect;
/**
* Instantiates $connect
*
* #param $connect
*/
function __construct()
{
$this->connect = new Connect($this, OAuth::consumer('Facebook'));
}
/**
* Login user with facebook
*
* #return void
*/
public function initialise() {
// TODO: Actually probably not needed as we'll control
// whether this controller is called via a filter or similar
if(Auth::user()) return Redirect::to('/');
return $this->connect->loginOrCreate(Input::all());
}
/**
* User authenticated, return to main game view
* #return Response
*/
public function facebookConnectSucceeds()
{
return Redirect::to('/');
}
}
So when the route is initialised I construct a new Connect instance and I pass an instance of $this class to my Connect class (to act as a listener) and call the loginOrCreate method.
Apes\Utilities\Connect
<?php
namespace Apes\Utilities;
use Apes\Creators\Account;
use Illuminate\Database\Eloquent\Model;
use \User;
use \Auth;
use \Carbon\Carbon as Carbon;
class Connect
{
/**
* #var $facebookConnect
*/
protected $facebookConnect;
/**
* #var $account
*/
protected $account;
/**
* #var $facebookAuthorizationUri
*/
// protected $facebookAuthorizationUri;
/**
* #var $listener
*/
protected $listener;
public function __construct($listener, $facebookConnect)
{
$this->listener = $listener;
$this->facebookConnect = $facebookConnect;
$this->account = new Account();
}
public function loginOrCreate($input)
{
// Not the focus of this test
if(!isset($input['code'])){
return $this->handleOtherRequests($input);
}
// Trying to stub this method is my main issue
$facebookUserData = $this->getFacebookUserData($input['code']);
$user = User::where('email', '=', $facebookUserData->email)->first();
if(!$user){
// Not the focus of this test
$user = $this->createAccount($facebookUserData);
}
Auth::login($user, true);
// I want to test that this method is called
return $this->listener->facebookConnectSucceeds();
}
public function getFacebookUserData($code)
{
// I can't seem to stub this method because it's making another method call
$token = $this->facebookConnect->requestAccessToken($code);
return (object) json_decode($this->facebookConnect->request( '/me' ), true);
}
// Various other methods not relevant to this question
I've tried to trim this down to focus on the methods under test and my understanding thus far as to what is going wrong.
Connect Spec
<?php
namespace spec\Apes\Utilities;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use \Illuminate\Routing\Controllers\Controller;
use \OAuth;
use \Apes\Creators\Account;
class ConnectSpec extends ObjectBehavior
{
function let(\FacebookConnectController $listener, \OAuth $facebookConnect, \Apes\Creators\Account $account)
{
$this->beConstructedWith($listener, $facebookConnect, $account);
}
function it_should_login_the_user($listener)
{
$input = ['code' => 'afacebooktoken'];
$returnCurrentUser = (object) [
'email' => 'existinguser#domain.tld',
];
$this->getFacebookUserData($input)->willReturn($returnCurrentUser);
$listener->facebookConnectSucceeds()->shouldBeCalled();
$this->loginOrCreate($input);
}
So here's the spec that I'm having issues with. First I pretend that I've got a facebook token already. Then, where things are failing, is that I need to fudge that the getFacebookUserData method will return a sample user that exists in my users table.
However when I run the test I get:
Apes/Utilities/Connect
37 ! it should login the user
method `Double\Artdarek\OAuth\Facade\OAuth\P13::requestAccessToken()` not found.
I had hoped that 'willReturn' would just ignore whatever was happening in the getFacebookUserData method as I'm testing that separately, but it seems not.
Any recommendations on what I should be doing?
Do I need to pull all of the OAuth class methods into their own class or something? It seems strange to me that I might need to do that considering OAuth is already its own class. Is there some way to stub the method in getFacebookUserData?
Update 1
So I tried stubbing the method that's being called inside getFacebookUserData and my updated spec looks like this:
function it_should_login_the_user($listener, $facebookConnect)
{
$returnCurrentUser = (object) [
'email' => 'existinguser#domain.tld',
];
$input = ['code' => 'afacebooktoken'];
// Try stubbing any methods that are called in getFacebookUserData
$facebookConnect->requestAccessToken($input)->willReturn('alongstring');
$facebookConnect->request($input)->willReturn($returnCurrentUser);
$this->getFacebookUserData($input)->willReturn($returnCurrentUser);
$listener->facebookConnectSucceeds()->shouldBeCalled();
$this->loginOrCreate($input);
}
The spec still fails but the error has changed:
Apes/Utilities/Connect
37 ! it should login the user
method `Double\Artdarek\OAuth\Facade\OAuth\P13::requestAccessToken()` is not defined.
Interestingly if I place these new stubs after the $this->getFacebookUserData stub then the error is 'not found' instead of 'not defined'. Clearly I don't fully understand the inner workings at hand :D
Not everything, called methods in your dependencies have to be mocked, because they will in fact be called while testing your classes:
...
$facebookConnect->requestAccessToken($input)->willReturn(<whatever it should return>);
$this->getFacebookUserData($input)->willReturn($returnCurrentUser);
...
If you don't mock them, phpspec will raise a not found.
I'm not familiar with the classes involved but that error implies there is not method Oauth:: requestAccessToken().
Prophecy will not let you stub non-existent methods.
I writing a Magento plugin and try to test it with the PHPUnit Testing Integration from http://www.ecomdev.org. Right now I'm trying to test a method which two different sets of config settings but for some reason the second one is never loaded and the first is used again, so the tests which expects the second fixture fails.
Here's the problem reduced to the important lines:
Modul.php (Model)
<?php
class MyModule_Module_Model_TestModel extends Mage_Payment_Model_Method_Abstract {
protected $sandbox;
public function __construct() {
$this->sandbox = $this->getConfigData('sandbox');
}
public function getSandboxSetting() {
return $this->sandbox;
}
}
?>
fixture config.yaml
config
default/payment/modul/sandbox: 0
fixture configSB.yaml
config
default/payment/modul/sandbox: 1
Modul.php (Test)
<?php
class MyModule_Module_Test_Model_TestModel extends EcomDev_PHPUnit_Test_Case {
public function setUp() {
parent::setUp();
$this->object = Mage::getModel('module/testmodel');
}
/**
* #test
* #loadFixture config
*/
public function testCorrectShopSettingsWithoutSandbox() {
$this->assertEquals('0', $this->object->getSandboxSetting());
}
/**
* #test
* #loadFixture configSB
*/
public function testCorrectShopSettingsWithSandbox() {
$this->assertEquals('1', $this->object->getSandboxSetting());
}
protected function tearDown() {
unset($this->object);
parent::tearDown();
}
}
?>
Unfortunately the second tests fails, no matter in which order they are performed. Actually the ecomdev test suite should discard the fixtures (I had a look at tearDown() in case.php) but the config data is still there and can't be overwritten. Is there a workaround or is this a problem with Magento / the test suite?
have you tried the latest version from github? We saw some issues related to config with the new Magento version, so in dev branch it was fixed.
Here is branch url:
https://github.com/IvanChepurnyi/EcomDev_PHPUnit/tree/dev