Now that we have the entire supporting infrastructure we need to begin working with, let's return to our first feature we defined at the design stage and define the acceptance test for it.
The main point with end-to-end acceptance tests is that we have to deal with our application using only its UI. We don't have any way of direct access to the database or, worse, filesystem around the application. So, to test a query for some data in the database, this data should be inserted into the database first. And it should be done using the UI.
Here are the resulting test steps:
So, this test forces us to have three user-interface pages: the new customer records, the customer list, and the query UI. That's part of why it's called "end-to-end" testing.
Translated to the Codeception acceptance test suite, the procedure just described will look like this:
$I = new AcceptanceTesterCRMOperatorSteps($scenario); $I->wantTo('add two different customers to database'), $I->amInAddCustomerUi(); $first_customer = $I->imagineCustomer(); $I->fillCustomerDataForm($first_customer); $I->submitCustomerDataForm(); $I->seeIAmInListCustomersUi(); $I->amInAddCustomerUi(); $second_customer = $I->imagineCustomer(); $I->fillCustomerDataForm($second_customer); $I->submitCustomerDataForm(); $I->seeIAmInListCustomersUi(); $I = new AcceptanceTesterCRMUserSteps($scenario); $I->wantTo('query the customer info using his phone number'), $I->amInQueryCustomerUi(); $I->fillInPhoneFieldWithDataFrom($first_customer); $I->clickSearchButton(); $I->seeIAmInListCustomersUi(); $I->seeCustomerInList($first_customer); $I->dontSeeCustomerInList($second_customer);
Let's put it as is inside the tests/acceptance/QueryCustomerByPhoneNumberCept.php
file. This is our goal for the current chapter.
Let's go over the not-so-obvious things in this test script.
First, we broke down the scenario into two logical parts and made two different subclasses of AcceptanceTester
to stress this distinction. Codeception has a nice helper to automatically generate subclasses of different Guy
classes, using which we created the AcceptanceTesterCRMOperatorSteps
class as shown in the following command:
$ cept generate:stepobject acceptance CRMOperatorSteps
Composer will prompt you for the names of the methods in the step where the object class is being generated. Just hit Enter there without writing anything to tell it that you want to start afresh.
This helper is used to support the StepObject pattern (see http://codeception.com/docs/07-AdvancedUsage#StepObjects), so it'll automatically append the Steps
suffix to the CRMOperatorSteps
class name. Of course, it's a lot more natural to reason about the subclasses of AcceptanceTester
as having different roles than having some abstract containers of steps. However, if we forcefully rename the generated classes, removing the unnecessary suffix, we would lose the auto-loading ability that Codeception automatically provides us with, so we'll just tolerate it. The preceding incantation places the CRMOperatorSteps.php
class file into the tests/acceptance/_steps
subdirectory.
In the same way, the CRMUserSteps
class can be generated.
Now, let's define the steps mentioned in the test scenario. Almost all of these high-level steps will just be the containers of the more low-level steps built-in with the acceptance tester shipped with Codeception.
First, we will see the CRM operator steps.
The "I am in Add Customer UI" step will just be an opening of the route corresponding to our future Add Customer UI, so it'll look like this:
function amInAddCustomerUi() { $I = $this; $I->amOnPage('/customers/add'), }
"Imagine Customer" is a helper to generate customer data to be entered in the Add Customer UI automatically and in a random way. Such placeholder data can be generated in any way. We'll be using the amazing Faker library (https://github.com/fzaninotto/Faker) to generate the correct-looking data for us. However, we'll look into that a bit later. Right now, the need to enter data in the Add Customer UI forces us to decide on the actual UI. We don't go after any fancy interface here; it'll be just a plain HTML form with the Submit button. But what fields should be there? Let's return to our Customer aggregate and see what parts of it we crucially need to fulfill our test scenario:
For simplicity, we leave off the E-mail and Address models for later. Of course, we don't take into account the Contract aggregate at all. For the task to be any "interesting", we include all the unique parts of the customer: Name, Birthday, and Notes. Do remember that Name is a structure and not just a line of text like Notes.
Now, let's settle on the fields in our Add Customer form. Please pay attention to the naming of form fields; it's not arbitrary and corresponds to our future database schema and the Yii 2 model's configuration. Have a look at the following table:
Field |
Name in form |
---|---|
Full Name |
|
Birth Date |
|
Notes |
|
Phone Number |
|
Note that while in our design one customer can have several phones, here we allow only one. We cannot implement features until we have a basic test done for it, and our test does not explicitly check the ability to enter several phone numbers (yet).
So, we can now define the CRMOperatorSteps.imagineCustomer
method. First of all, let's bring the Faker library into our project:
$ php composer.phar require "fzaninotto/faker:*"
Then, let's consider a customer with the attributes defined in the following method:
public function imagineCustomer() { $faker = FakerFactory::create(); return [ 'CustomerRecord[name]' => $faker->name, 'CustomerRecord[birth_date]' => $faker->date('Y-m-d'), 'CustomerRecord[notes]' => $faker->sentence(8), 'PhoneRecord[number]' => $faker->phoneNumber ]; }
Our imagination here makes a structure for us, which we can easily use in our fillCustomerData
method:
function fillCustomerDataForm($fieldsData) { $I = $this; foreach ($fieldsData as $key => $value) $I->fillField($key, $value); }
The method to submit the form will be straightforward; let's just name the button Submit:
function submitCustomerDataForm() { $I = $this; $I->click('Submit'), }
Then, we need only two methods, one for checking whether we are inside the Customer List UI and another to actually go there:
public function seeIAmInListCustomersUi() { $I = $this; $I->seeCurrentUrlMatches('/customers/'), } function amInListCustomersUi() { $I = $this; $I->amOnPage('/customers'), }
In the Codeception ideology, assertion methods are expected to have the see
prefix on the names, so we adhere to it.
We use CurrentUrlMatches
to match URLs using regular expressions instead of the more strict CurrentUrlEquals
, because we assume there'll be some query parameters at the end of the URL.
With all these methods defined in the CRMOperatorSteps
class, we now have the first half of our test case completed (which means runnable).
Let's get done with the test steps for a CRM user, who'll do the querying. The following changes are to be done in the CRMUserSteps
class. First, the obvious one is as follows:
function amInQueryCustomerUi() { $I = $this; $I->amOnPage('/customers/query'), }
Let's just name the field to enter the phone number in the same way we named it in the Add Customer form as follows:
function fillInPhoneFieldWithDataFrom($customer_data) { $I = $this; $I->fillField( 'PhoneRecord[number]', $customer_data['PhoneRecord[number]'] ); }
Let's name the button to start searching for customer data as the Search button as follows:
function clickSearchButton() { $I = $this; $I->click('Search'), }
Then, we arrive at the duplication of CRMOperatorSteps.seeIAmInListCustomersUi
:
function seeIAmInListCustomersUi() { $I = $this; $I->seeCurrentUrlMatches('/customers/'), }
Let's just stick to the Rule of Three proposed in Refactoring: Improving the Design of Existing Code, Martin Fowler, Kent Beck, John Brant, William Opdyke, and Don Roberts, Addison-Wesley Professional, and let this method be as it is.
Lastly, here are our assertions:
function seeCustomerInList($customer_data) { $I = $this; $I->see($customer_data['CustomerRecord[name]'], '#search_results'), } function dontSeeCustomerInList($customer_data) { $I = $this; $I->dontSee($customer_data['CustomerRecord[name]'], '#search_results'), }
We should note that this is an extremely simple implementation, and it relies on several assumptions, which will be held true at this stage of development:
search_results
Let's keep this test as it is for simplicity, but when we have more than one search result, we should think about how to properly check for a particular search result's existence (and most probably, the default semantics of the see
method will not be enough for us).
Quite an important question is why we don't check for the customer data being shown in the Customer List UI after each addition of a new customer. After we query for the phone number, we end up in the same Customer List UI after all.
Well, for now, the reasoning is dead simple: our goal is to check that we can query for a customer by the phone number. Also, the existence of some assertions halfway through the test case would violate the Single Assertion principle (explained in detail in Clean Code, Robert Martin, Prentice Hall). However, as this is the end-to-end acceptance test, doing this is probably not such a bad thing. Anyway, nothing prevents us from extending this test further (again, it's an acceptance test imitating the behavior of a real user), but for now, let's stick with this simple scenario.
If you run the completed test scenario now, you should get the following error message:
1) Failed to add two different customers to database in QueryCustomerByPhoneNumberCept
Sorry, I couldn't fill field "CustomerRecord[first_name]","Cheyanne":
Field by name, label, CSS or XPath 'CustomerRecord[first_name]' was not found on page.
Scenario Steps:
2. I fill field "CustomerRecord[first_name]","Cheyanne"
1. I am on page "/customers/add"
We get this error because we don't have the HTML form served for the /customers/add
request.
We have finally arrived at the Yii 2 installation procedure.
We're going to make a totally custom application, which will not have the intention to rely much on the directory structure conventions of the Yii framework, instead just using its classes where it's convenient.
First of all, you need to declare Yii 2 as a dependency of your application.
Either add the required require
line for Yii 2 manually in the composer.json
file or run the following command:
$ php composer.phar require "yiisoft/yii2:*"
If you edited the manifest manually, do not forget to call the installation command:
$ php composer.phar install
Composer will bring Yii 2 to your code base after that. It should end inside the vendor/yiisoft/yii2
folder.
Yii 2 includes an important feature, which is a built-in requirement checker. When you install one of the application templates discussed in Chapter 1, Getting Started, you get a script named requirements.php
in the root of your code base. It's very useful, so make a copy and place it into the web
subfolder. You can download this file from the Yii 2 repository at https://github.com/yiisoft/yii2/blob/master/apps/basic/requirements.php. After you get the file, you can run it from the command line by calling the following:
$ php web/requirements.php
Alternatively, you can just point your browser at the URL http://<your_domain>/requirements.php
and get a nicely laid-out page with detailed explanations about whether your deploy target satisfies the framework requirements.
A really high-level explanation of things is as follows. To serve any of the requests coming to the application, Yii uses a single physical PHP script that instantiates a special object of the yiiwebApplication
class. This object uses Yii's interpretation the of Model View Controller (MVC) composite pattern to process the request and display the result back to the sender. If you forgot or were unaware of the MVC before, then you probably want to read at least the official documentation for Yii for an in-depth explanation.
The Yii interpretation of MVC is as follows:
The most subtle part of this scheme is the model concept. Depending on the interpretations, a model is either what the controller uses to get data to put into a view or actually is what the controller puts into a view. Yii 2 does not enforce any approach, but its implementation of models assumes that the model is a container for some data, either transient (in-memory only) or persistent (with the support of the active record pattern).
So, a request passes through the following steps:
index.php
script.Application
object is created. It decides which Controller
class should be used to handle this request.Controller
object is created. It decides what Action
it should run (they can be either methods of Controller
or separate classes), and run it, passing the request details there.Knowing these steps, let's modify our current entry script, so it'll render the same thing, but using the Yii framework instead of displaying a raw text output. We'll look at a nice flowchart with all the details in Chapter 12, Route Management.
Right now, we should have the following project structure:
We will start by introducing Yii 2 from the entry point script. At a reasonable minimum, the index.php
file should look like this:
<?php // Including the Yii framework itself (1) require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php'), // Getting the configuration (2) $config = require(__DIR__ . '/../config/web.php'), // Making and launching the application immediately (3) (new yiiwebApplication($config))->run();
At (1) in the code, we bring all that Yii needs to function properly to our environment.
At (2) in the code, we get the configuration tree for the application. Yii application config is a massive PHP array describing the initial values of attributes of the application itself as well as its various components.
At (3) in the code, we make a new instance of the Application
subclass representing web applications and immediately call the method called run
on it.
At (2) in the code we are loading a nonexistent file config/web.php
. Let's make it:
<?php return [ 'id' => 'crmapp', 'basePath' => realpath(__DIR__ . '/../'), 'components' => [ 'request' => [ 'cookieValidationKey' => 'your secret key here', ], ],];
We must specify three settings:
id
: This is a mandatory identifier for our application. It is required because later we can split our application into different so-called modules, which we refer to using these IDs, and the top-level application obeys the same rules as a common module.basePath
: This is mandatory because it's basically the way for Yii to understand where it exists in the host filesystem. All relative paths allowed in other settings should start from here.components.request.cookieValidationKey
: This is a leak from the user authentication subsystem, which we will discuss in Chapter 5, User Authentication. This setting is the private key for validating users using the remember me feature, which relies on cookies. In earlier beta versions of Yii 2, this key was generated automatically. It was made visible by the 4e4e76e8 commit (https://github.com/yiisoft/yii2/commit/4e4e76e8838cbe097134d6f9c2ea58f20c1deed6). Apart from this setting, you can set components.request.enableCookieValidation
to false
, thus disabling cookie-based authentication completely. This will get your application working, too.Next, we'll add some mandatory folders, because Yii just throws exceptions in case they are not there, and doesn't create them itself: web/assets
and runtime
. These folders are used by the framework when the application is running.
Each controller class should have three distinct qualities:
controllerNamespace
setting of the Application
class.Controller
suffix in the name.yiiaseController
class. In the case of the controllers that are meant to be used by the web application and not the console one, we should extend the yiiwebController
class instead.In addition, it's important to understand how Yii 2 will actually search for controller classes.
Yii 2, in general, utilizes the autoloader compatible with the PSR-4 standard (http://www.php-fig.org/psr/psr-4/). To put it simply, such an autoloader treats namespaces as paths in the filesystem, given that there is a special root namespace defined, which maps to the root folder of the code base.
In our case, Yii 2 defines the app
namespace for us, which maps to the root folder of the code base. As a result, for example, the default value of the controllerNamespace
setting of the application, which is appcontrollers
, will map to the directory called controllers
at the root of code base, so all controller class definitions must be placed in there.
Also, each class to be available through the Yii 2 autoloader has to be put into its own file named exactly like the class itself.
So, let's create our first controller to satisfy the smoke test. We will not change the default controller namespace setting, and so we will write the following piece of code in the controllers/SiteController.php
file:
namespace appcontrollers; use yiiwebController; class SiteController extends Controller { public function actionIndex() { return 'Our CRM'; } }
This code heavily relies on the conventions of Yii. Without delving deep into the topic of routing in Yii, we can say that without special settings defined, Yii uses the actionIndex
method of the controller named SiteController
for the "/" request.
The most basic and straightforward way to define controller actions is to define them as public methods of the controllers having the name prefixed by action
. To get into the SiteController.actionIndex
method explicitly, you should request site/index.php
.
So, we have our smoke test passing with the Yii-based routing. Let's add some helpers for ease of debugging.
You can get a lot of strange errors at this stage of development. Let's look at the ways you can use to set up your application quickly and gather as much feedback as possible.
First of all, when you make a really dire mistake, such as not defining id
or basePath
in the application config, you basically get a blank page as a response from the Yii application. The only place you can look at is the logs of your web server. For example, in Apache you can use the ErrorLog
directive to specify the file in which such game-breaking error reports will end. Of course, any other application error will end there, regardless of whether it was rendered in the browser.
To fight off the "white screen," you can add a force override display_errors
setting in your index.php
entry point right after you require the Yii library, but before the creation and execution of the Application
object as follows:
ini_set('display_errors', true);
Also, you should add one handy constant before you require the Yii library. It's extremely important to define it before you require the Yii library, because the Yii library will define it in the event that you don't define it yourself. That's it. Here's how you do it:
define('YII_DEBUG', true);
This will change the application to debug mode, and if some nasty exceptions are thrown, you'll get the generic status 500 page or blank screen but also a detailed report from Yii with the most important lines highlighted.
Lastly, you can add a custom logger to your application, which will log the errors happening within the application to a file. Chapter 8, Overall Behavior, explains this in great detail.
3.137.176.166