(For more resources on Agile, see here.)
TrackStar is a Software Development Life Cycle (SDLC) issue management application. Its main goal is to help keep track of all the many issues that arise throughout the course of building software applications. It is a user-based application that allows the creation of user accounts and grants access to the application features, once a user has been authenticated and authorized. It allows a user to add and manage projects.
Projects can have users associated with them (typically the team members working on the project) as well as issues. The project issues will be things such as development tasks and application bugs. The issues can be assigned to members of the project and will have a status such as not yet started, started, and finished. This way, the tracking tool can give an accurate depiction of projects with regard to what has been accomplished, what is currently in progress, and what is yet to be started.
Creating user stories
Simple user stories are a great way to identify the required features of your application. User stories, in their simplest form, state what a user can do with a piece of software. They should start simple, and grow in complexity as you dive into more and more of the details around each feature. Our goal here is to begin with just enough complexity to allow us to get started. If needed, we'll add more detail and complexity later.
We briefly touched on the three main entities that play a large role in this application: users, projects, and issues. These are our primary domain objects, and are extremely important items in this application. So, let's start with them.
TrackStar is a user-based web application. There will be two high-level user types:
An anonymous user is any user of the application that has not been authenticated through the login process. Anonymous users will only have access to register for a new account or to log in. All other functionality will be restricted to authenticated users.
An authenticated user is any user that has provided valid authentication credentials through the login process. In other words, authenticated users are logged-in users. They will have access to the main features of the application such as creating and managing projects, and project issues.
Managing the project is the primary purpose of the TrackStar application. A project represents a general, high-level goal to be achieved by one or more users of the application. The project is typically broken down into more granular tasks (or issues) that represent the smaller steps that need to be taken to achieve the overall goal.
As an example, let's take what we are going to be doing throughout this book, that is, building a project and issue tracking management application. Unfortunately, we can't use our yet-to-be-created application as a tool to help us track its own development. However, if we were using a similar tool to help track what we are building, we might create a project called Build The TrackStar Project/Issue Management Tool. This project would be broken down into more granular project issues such as 'Create the login screen' or 'Design database schema for issues', and so on.
Authenticated users can create new projects. The creator of the project within an account has a special role within that project, called the project owner. Project owners have the ability to edit and delete these projects as well as add new members to the project. Other users associated with the project—besides the project owner—are referred to simply as project members. They have the ability to add new issues, as well as edit existing ones.
Project issues can be classified into one of the following three categories:
- Features: Items that represent real features to be added to the application. For example, 'Implement the login functionality'
- Tasks: Items that represent work that needs to be done, but is not an actual feature of the software. For example, 'Set up the build and integration server'
- Bugs: Items that represent application behaviors that are not working as expected. For example, 'The account registration form does not validate the format of input e-mail addresses'
Issues can have one of the following three statuses:
- Not yet started
Project members can add new issues to a project, as well as edit and delete them. They can assign issues to themselves or other project members.
For now, this is enough information on these three main entities. We could go into a lot more detail about what exactly account registration entails' and how exactly one adds a new task to a project', but we have outlined enough specifications to begin on these basic features. We'll nail down the more granular details as we proceed with the implementation.
However, before we start, we should jot down some basic navigation and application workflow. This will help everyone to better understand the general layout and flow of the application we are building.
Navigation and page flow
It is always good to outline the main pages within an application, and how they fit together. This will help us quickly identify some needed Yii controllers, actions and views as well as help to set everyone's expectations as to what we'll be building towards at the onset of our development.
The following figure shows the basic idea of the application flow from logging in, through the project details listing:
When users first come to the application, they must log in to authenticate themselves before accessing any functionality. Once successfully logged-in, they will be presented with a list of his current projects along with the option to create a new project. Choosing a specific project will take them to the project details page. The project details page will present a list of the issues by type. There will also be the option to add a new issue as well as edit any of the listed issues.
This is all pretty basic functionality, but the figure gives us a little more information on how the application is stitched together and allows us to better identify our needed models, views, and controllers. It also allows something visual to be shared with others so that everyone involved has the same 'picture' of what we are working towards. In my experience, almost everyone prefers pictures over written specifications when first thinking through a new application.
Defining a data scheme
We still need to think a little more about the data we will be working with as we begin to build toward these specifications. If we pick out all the main nouns from our system, we may end up with a pretty good list of domain objects and, by extension of using Active Record, the data we want to model. Our previously outlined user stories seem to dictate the following:
- A User
- A Project
- An Issue
Based on this and the other details provided in the user stories and application workflow diagram, a first attempt at the needed data is shown in the following figure.
This is a basic object model that outlines our primary data entities, their respective attributes, and some of the relationships between them. The 1..* on either side of the line between the Project and User objects represents a many-to-many relationship between them. A user can be associated with one or more projects, and a project has one or more users. Similarly we have represented the fact that a project can have zero or more issues associated with it, whereas an issue belongs to just one specific project. Also, a user can be the owner of (or requester of) many issues, but an issue has just one owner (and also just one requester).
We have kept the attributes as simple as possible at this state. A User is going to need a username and a password in order to get past the login screen. The Project has only a name
Issues have the most associated information based on what we currently know about them. As discussed briefly in the user stories above, they will have a type attribute to distinguish the general category (bug, feature, or task). They will also have a status attribute to indicate the progress of the issue being worked on. A user in the system will initially create the issue, this is the requester. Once a user in the system has been assigned to work on the issue, they will be the owner of the issue. We have also defined the description attribute to allow for some descriptive text of the issue to be entered.
Notice that we have not explicitly talked about schemas or databases yet. The fact is, until we think through what is really needed from a data perspective, we won't know the right tool to use to house this data. Would flat files on the filesystem work just as well as a relational database? Do we need a persistent data at all?
The answers to these questions are not needed in this early planning state. It is better to focus more on the features that we want and the type of data needed to support these features. We can turn to the explicit technology implementation details after we have had a chance to discuss these ideas with other project stakeholders to ensure we are on the right track. Other project stakeholders include anyone and everyone involved in this development project. This can include the client, if building an application for someone else, as well as other development team members, product/project managers, and so on. It is always a good idea to get some feedback from "the team" to help validate the approach and any assumptions being made.
However, before we dive right into building our application, we need to cover our development approach. We will be employing some specific development methodologies and principles, and it makes sense to go over these prior to getting started with coding.
Defining our development methodology
We will be employing an agile inspired process of iterative and incremental development as we build this application. 'Agile' is certainly a loaded term in modern software development and can have varied meanings among developers. Our process will focus on the aspects of an agile methodology that embrace transparent and open collaboration, constant feedback loops, and a strong ability to respond quickly to changing requirements.
We will work incrementally in that we won't wait until every detail of the application has been specified before we start coding. Once the details of a particular feature have been finalized, we can begin work on implementing that feature, even though other features or application details are still in the design/planning stage.
The process surrounding this feature implementation will follow an iterative model. We will do some initial iteration planning, engage in analysis and design, write the code to try out these ideas, test the code, and gather feedback. We then repeat this cycle of design->code->test->evaluation, until everyone is happy. Once everyone is happy, we can deploy the application with the new feature, and then start gathering the specifications on the next feature(s) to be implemented in the next iteration.
Automated software testing
Gathering feedback is of fundamental importance to agile development. Feedback from the users of the application and other project stakeholders, feedback from the development team members, and feedback directly from the software itself. Developing software in a manner that will allow it to tell you when something is broken can turn the fear associated with integrating and deploying applications into boredom. The method by which you empower your software with this feedback mechanism is writing unit and functional tests, and then executing them repeatedly and often.
Unit and functional testing
Unit tests are written to provide the developer with verification that the code is doing the right things. Functional tests are written to provide the developer, as well as other project stakeholders, that the application, as a whole, is doing things the right way.
Unit tests are tests that focus on the smallest units within a software application. In an object-oriented application, (such as a Yii web application) the smallest units are the public methods that make up the interfaces to classes. Unit tests should focus on one single class, and not require other classes or objects to run. Their purpose is to validate that a single unit of code is working as expected.
Functional tests focus on testing the end-to-end feature functionality of the application. These tests exist at a higher level than the unit tests and typically do require multiple classes or objects to run. Their purpose is to validate that a given feature of the application is working as expected.
Benefits of testing
There are many benefits to writing unit and functional tests. For one, they are a great way to provide documentation. Unit tests can quickly tell the exact story of why a block of code exists. Similarly, functional tests document what features are implemented within an application. If you stay diligent in writing these tests, then the documentation continues to evolve naturally as the application evolves.
They are also invaluable as a feedback mechanism to constantly reassure the developer and other project stakeholders that the code and application is working as expected. You run your tests every time you make changes to the code and get immediate feedback on whether or not something you altered inadvertently changed the behavior of the system. You then address these issues immediately. This really increases the confidence that developers have in the application's behavior and translates to fewer bugs and more successful projects.
This immediate feedback also helps to facilitate change and improving the design of the code base. A developer is more likely to make improvements to existing code if a suite of tests are in place to immediately provide feedback as to whether the changes made altered the application behavior. The confidence provided by a suite of unit and functional tests allows developers to write better software, release a more stable application, and ship quality products.
Test-driven development (TDD) is a software development methodology that helps to create an environment of comfort and confidence by ensuring your test suite grows organically with your application, and is always up-to-date. It does this by stipulating that you begin your coding by first writing a test for the code you are about to write. The following steps sum up the process:
- Begin by writing a test that will quickly fail.
- Run the test to ensure it does, indeed, fail.
- Quickly add just enough code to the class you are testing to get the test to pass.
- Run the test again to ensure it does, indeed, pass.
- Refactor the code to remove any repetitive logic or improve any corners cut while you were just trying to get the test to pass.
These steps are then repeated throughout the entire development process.
Even with the best intentions, if you wait to write your tests until after the code is completed, you probably won't. Writing your tests first and injecting the test writing process directly into the coding process will ensure the best test coverage. This depth of coverage will help minimize the stress and fear that can accompany complex software applications and build confidence by constantly providing positive feedback as additions and changes are made.
In order to embrace a TDD process, we need to understand how to test within a Yii application.
(For more resources on Agile, see here.)
Testing in Yii
As of version 1.1, Yii is tightly integrated with the PHPUnit (http://www.phpunit.de/) and Selenium Remote Control (http://seleniumhq.org/projects/remotecontrol/) testing frameworks. There is nothing about TDD that presupposes a particular testing framework (or any testing framework at all, for that matter), but using one is strongly recommended.
You may certainly test Yii PHP code with any of the testing frameworks available. However, the tight integration of Yii with the two frameworks mentioned previously makes things even easier. And making things easy is one of our primary goals here. We will be using the testing features of Yii as we proceed.
When we use the yiic webapp console command to create our new Hello World demo application, many files and folders are automatically created for us. The ones among these relevant to writing and performing automated tests are the following:
Name of folder
This contains protected application files
This contains tests for the application
This contains database fixtures
This contains functional tests
This contains unit tests
This contains coverage reports
The script executed at the very beginning of the tests
The PHPUnit configuration file
The base class for Web-based functional tests
We will be placing our tests into three main folders: fixtures, functional, and unit. The report folder is used to store the generated code coverage reports.
Note: The PHP extension, XDebug, must be installed in order to generate reports. It is recommended to use PECL to install XDebug. For details on this installation, see http://xdebug.org/docs/install. This is not a requirement if you wish to simply follow along with our examples.
A unit test in Yii is written as a PHP class that extends from the framework class, CTestCase. The conventions prescribe it be named AbcTest where Abc is replaced by the name of the class being tested.
The test class primarily has a set of test methods named testXyz where Xyz is often the same as the method name the test is built for in the class being tested.
Continuing with the MessageController example, if we were testing our actionHelloworld() method, we would name the corresponding test method in our MessageTest class, testActionHelloworld().
In order to follow along with our unit-testing approach, you will need to install PHPUnit. This should be done using the Pear Installer (for more information on Pear, see http://pear.php.net/) For Mac OS users, this is a simple as issuing two commands from the command line:
% sudo pear channel-discover pear.phpunit.de
% sudo pear install phpunit/PHPUnit
However, your configuration may differ slightly. For more information on this installation process see: http://www.phpunit.de/manual/3.0/en/installation.html
It is certainly beyond the scope of this article to specifically cover PHPUnit testing features. It is recommended that you take some time to go through the documentation (http://www.phpunit.de/wiki/Documentation) to get a feel for the jargon and learn how to write basic unit tests.
Much like units tests, functional tests are written as PHP classes. However, they extend from CWebTestCase rather than CTestCase. The conventions are the same in that we name our functional test class AbcTest where Abc is the class being tested, and save the class in a file named AbcTest.php. However, we store these under the folder protected/tests/functional.
In order to run functional tests, you need to install Selenium.
In addition to PHPUnit, the Selenium Remote Control Server (Selenium RC) is needed in order to run the functional tests. Installing Selenium RC is very simple.
- Download Selenium Remote Control (Selenium RC) zip file from http://seleniumhq.org/download/.
- Unpack the zip file to a preferred location on your system.
The contents of the unzipped folder will have several specific client-based folders and one that contains the actual RC server. It will be named something similar to
Where x will be specific to the version downloaded. Starting the server is also simple. Just navigate to this server folder on your system and issue:
% java -jar selenium-server.jar
This will start the server in that console.
Running a quick example
The TDD approach we will be taking throughout building the TrackStar application will primarily focus on the writing and executing of unit tests. However it would be a shame not to run though at least one functional test example. The site we create for our demo Hello World application has an example functional test located at protected/tests/functional/SiteTest.php. This file has three test methods created within it. One for testing the main home page, one for testing the contact page, and a third for testing the login and logout functionality.
Before we can run this functional test, we need to make a couple of configuration changes to our application. First we need to alter protected/tests/WebTestCase. php to properly define our test URL that Selenium will attempt to open when it runs the tests. Open up that file and make sure the TEST_BASE_URL definition matches the URL to your demo application, that is, change the following line: define('TEST_BASE_URL','http://localhost/testdrive/indextest. php/');
The next change may only apply to Mac OS users. Although, if you are using Windows but prefer not to use Internet Explorer as the testing browser, then you may also want to make this change. The file protected/tests/phpunit.xml houses some configuration settings for Selenium Server. It is configured to use IE as the primary browser. We can remove the following highlighted line of code to ensure only Firefox will be used when running Selenium:
<browser name="Internet Explorer" browser="*iexplore" />
<browser name="Firefox" browser="*firefox" />
Now, as long as you have installed PHPUnit (see earlier Unit test section) and have ensured that Selenium Server is running, and then we can navigate to our tests folder at the command prompt and run this functional test:
% cd protected/tests/
% phpunit functional/SiteTest.php
What should happen is that you will see your browser being automatically invoked, as the Selenium Server platform is using the browser to access the end-user functionality of the site that we configured in the WebTestCase.php file. As it runs through the test methods, it actually automates the behavior of a real user of the site. Pretty cool!
If everything worked, the end results should display back in the command line window where we executed the test. Something similar to the following will be displayed::
Time: 19 seconds, Memory: 10.25Mb
OK (3 tests, 10 assertions)
Being able to automate these end-user functional tests is a fantastic way to begin to automate your quality assurance testing (QA testing). If you have a separate QA team on the project, it would be very beneficial to show them how to use this tool to test the application. As mentioned, we will be focused more on writing unit tests than these end-user browser executed functional tests, as we employ a test-driven approach. However, having a test suite that covers both unit and functional tests is the best approach to ensuing the best quality in the application development.
It might be the case that one of your functional tests failed when running the SiteTest.php tests. If the results of your test indicated a failure at line 44 of the SiteTest.php file, you may need to slightly alter this line to get your tests to pass. This depends on the way the logout link in the main menu displays. The autogenerated test might expect the link read just Logout rather than Logout (demo). If your functional test fails for this reason, simply change that line to read just as the logout link would read if you had logged in as demo/demo, like this:
We have a working application that displays Hello World! and Goodbye, Yii Developer. The two action methods handling the requests to display these messages are in our MessageController class.
Let's add some new behavior to MessageController.php. Let's enhance it to be able to take in any message string, and simply return that exact message string back to the caller. That sounds simple enough. We should just open up MessageController.php and add a new public method, maybe called repeat() and have it do what we just described, right? Well, not quite. As we are taking a TDD approach, we should start by writing a test for this new behavior.
As we are testing the behavior of a class method, we need to write a unit test. Following the Yii defaults, this unit test should reside in the protected/tests/unit/ folder and be called MessageTest.php. Taking small steps, let's just add a new class by this name and have it extend the base Yii Framework class for unit tests, CTestCase.
Create the new file, protected/tests/unit/MessageTest.php and add to it the following code:
class MessageTest extends CTestCase
Now we can navigate to our tests folder and execute the command to run this test:
The following will be displayed after execution:
PHPUnit 3.3.17 by Sebastian Bergmann.
Time: 0 seconds
There was 1 failure:
No tests found in class "MessageTest".
Tests: 1, Assertions: 0, Failures: 1.
Our test failed. It tells us we don't have a test defined in our test class. This is certainly true, as we have not coded one yet. But we have started down the first step of TDD, which is to quickly write a test that fails (though one could argue we have not really written an actual test as of yet).
Let's add a test method. As we are writing a test to validate our MessageController's ability to repeat back a string fed to it, let's call this test testRepeat. Add the following code so that our test class looks like:
class MessageTest extends CTestCase
public function testRepeat()
If we rerun the test now, we will get the following results:
OK (1 test, 0 assertions)
This is certainly a step in the right direction. We have followed the TDD second step, which is writing just enough code to get the test to pass. Of course, it passed because the method does not test anything at all, so this is not terribly useful. Nonetheless, is a small step in the right direction. As there is nothing to really refactor here, let's repeat the TDD process back at the top: Quickly write a test that fails.
This time, we will add to the test so it does something. That "something" will be testing the specific repeat behavior of MessageController. In order to do that, we need to:
- Create a new instance of our MessageController class in the test method.
- Call a method on it by feeding it a string of text.
- Verify that the returned string is the same as the input string.
Let's add the code to do that now. Alter the testRepeat() method to be:
public function testRepeat()
$message = new MessageController('messageTest');
$yell = "Hello, Any One Out There?";
$returnedMessage = $message->repeat($yell);
We have created a new MessageController class by providing a required controllerId to its constructor. We then built a string called $yell that we want repeated back. We call the repeat() method with this input string and capture the returned output in the variable $returnedMessage.
As we expect the returned message to contain the exact same string as was sent, we use the phpUnit API method assertEquals() that will compare the first string to the second and result in true or false depending on whether or not they are indeed equal. Now that we have a test written, let's try running it:
PHPUnit 3.4.12 by Sebastian Bergmann.
Time: 0 seconds, Memory: 10.00Mb
There was 1 error:
include(MessageController.php): failed to open stream: No such
file or directory
Tests: 1, Assertions: 0, Errors: 1.
I realize this may feel like we have taken a step backward. But, this is really within the nature of TDD. Small steps, testing all the time, and working to quickly get the test to first fail, and then pass. The error is telling us it is trying to create a new instance of MessageController, but it can't find the class in the classpath. We need to include the class as part of the unit test file. As we are running this within the context of the application, we can use the Yii::import syntax to include the class we want to test.
The Yii::import method allows us to quickly include the definition of a class. It differs from include and require in that it is more efficient. The class definition being imported is actually not included, until it is referenced for the first time. Importing the same namespace multiple times is also much faster than include_once and require_once. Also, it is good to note that when referring to a class defined by the Yii Framework, we do not need to import or include it. All core Yii classes are pre-imported.
Alter the MessageTest.php file to include the following line at the top:
class MessageTest extends CTestCase
Saving and running the test again does seem to get us past the previous error, but we are met with another. Now it fails due to the fact that we are calling a method named repeat() on our MessageController class, but this class does not have such a method. This certainly makes sense, as we have not added this method yet. Finally, it is time to code our new method, and get this test to pass.
Add the following method to the MessageController class:
public function repeat($inputString)
Save and run the test again:
% phpunit unit/MessageTest.php
OK (1 test, 1 assertion)
Bingo! We have a passing test. We can clean up our test code and refactor just a little by being a bit more compact in our writing and move some things inline:
public function testRepeat()
$message = new MessageController('messageTest');
$this->assertEquals($message->repeat("Any One Out There?"),
"Any One Out There?");
You should run the test one last time to ensure it still passes, as expected.
If TDD is new to you, all of this may seem a little strange, especially given all of the small, at times excruciatingly small, steps we took to achieve such a trivial method implementation. This was mostly to help underscore the rhythm of TDD. The size of the steps you take when using TDD is more of an art than a science. Start out super small and then take bigger steps as you become more confident and comfortable.
In several of the testing frameworks available to developers, the test results are displayed as color-coded. Tests that pass are displayed in green, and tests that fail are displayed in red. This also follows the familiar stoplight metaphor. For this reason, TDD is often summed up as: Red, Green, Refactor, Repeat. This refers to the basic rhythm that the previous example underscored:
- Red: Quickly add a new test and then run all the tests and see the new one fail.
- Green: Make a little change, just enough to get the failed test to pass.
- Refactor: If necessary, remove any duplication in the code or things you did just to make the test pass quickly, but otherwise makes you feel icky.
- Repeat: Start over with #1.
This chapter introduced the task tracking application, TrackStar.We talked about what the application is and what it does and provided some high-level requirements for the application in the form of informal user stories. We then identified some of the main domain objects we will need to create as well as worked through some of the data we will need to be able to house and manage.
We covered a basic agile-inspired development methodology and how we will be applying it to our process. We also introduced Test Driven Development (TDD) as one of the agile methodologies we will be embracing. We covered what TDD is in the abstract, but also how to implement it within the testing framework provided by Yii.
- Agile Works Best in PHP Projects [Article]