Day 11: Testing your Forms

In day 10, we created our first form with symfony. People are now able to post a
new job on Jobeet but we ran out of time before we could add some tests. That's
what we will do along these lines. Along the way, we will also learn more about
the form framework.

sidebar

Using the Form Framework without symfony

The symfony framework components are quite decoupled. This means that most of
them can be used without using the whole MVC framework. That's the case for
the form framework, which has no dependency on symfony. You can use it in any
PHP application by getting the lib/form/, lib/widgets/, and
lib/validators/ directories.

Another reusable component is the routing framework. Copy the lib/routing/
directory in your non-symfony project, and benefit from pretty URLs for free.

The components that are symfony independent form the
symfony platform:

We have already used the click() method to simulate clicks on links. The same
click() method can be used to submit a form. For a form, you can pass
the values to submit for each field as a second argument of the method. Like a
real browser, the browser object will merge the default values of the form with
the submitted values.

But to pass the field values, we need to know their names. If you open the
source code or use the Firefox Web Developer Toolbar "Forms > Display Form
Details" feature, you will see that the name for the company field is
jobeet_job[company].

note

When PHP encounters an input field with a name like jobeet_job[company], it
automatically converts it to an array of name jobeet_job.

To make things look a bit more clean, let's change the format to job[%s] by
adding the following code at the end of the configure() method of
JobeetJobForm:

The form we have submitted should be valid. You can test this by using the form tester:

with('form')->begin()->
hasErrors(false)->
end()->

The form tester has several methods to test the current form status, like the
errors.

If you make a mistake in the test, and the test does not pass, you can use the
with('response')->~debug|Debug~() statement we have seen during day 9. But you
will have to dig into the generated HTML to check for error messages. That's not
really convenient. The form tester also provides a debug() method that outputs
the form status and all error messages associated with it:

The criteria can be an array of values like above, or a Criteria instance for
more complex queries. You can test the existence of objects matching the
criteria with a Boolean as the third argument (the default is true), or the
number of matching objects by passing an integer.

The hasErrors() method can test the number of errors if passed an integer. The
isError() method tests the error code for a given field.

tip

In the tests we have written for the non-valid data submission, we have not
re-tested the entire form all over again. We have only added tests for
specific things.

You can also test the generated HTML to check that it contains the error
messages, but it is not necessary in our case as we have not customized the form
layout.

Now, we need to test the admin bar found on the job preview page. When a job has
not been activated yet, you can edit, delete, or publish the job. To test those
three links, we will need to first create a job. But that's a lot of copy and
paste. As I don't like to waste e-trees, let's add a job creator method in the
JobeetTestFunctional class:

If you remember from day 10, the "Publish" link has been configured to be called
with the HTTP PUT method. As browsers don't understand
PUT requests, the link_to() helper converts the link to a form with some
JavaScript. As the test browser does not execute JavaScript, we need to force
the method to PUT by passing it as a third option of the click() method.
Moreover, the link_to() helper also embeds a CSRF token as we have enabled
CSRF protection during the very first day; the _with_csrf option simulates
this token.

But if you run the tests, you won't have the expected result as we forgot to
implement this security measure yesterday. Writing tests is also a
great way to discover bugs, as you need to think about all edge cases.

Fixing the bug is quite simple as we just need to forward to a 404 page if the
job is activated:

The fix is trivial, but are you sure that everything else still works as
expected? You can open your browser and start testing all possible combinations
to access the edit page. But there is a simpler way: run your test suite; if you
have introduced a regression, symfony will tell you right away.

When a job is expiring in less than five days, or if it is already expired, the
user can extend the job validation for another 30 days from the current date.

Testing this requirement in a browser is not easy as the expiration date is
automatically set when the job is created to 30 days in the future. So, when
getting the job page, the link to extend the job is not present. Sure, you can
hack the expiration date in the database, or tweak the template to always
display the link, but that's tedious and error prone. As you have already
guessed, writing some tests will help us one more time.

The fromArray() method takes an array of values and updates the corresponding
column values. Does this represent a security issue? What if someone
tries to submit a value for a column for which he does not have authorization?
For instance, can I force the token column?

When submitting the form, you must have an extra_fields global error. That's
because by default forms do not allow extra fields to be present in the
submitted values. That's also why all form fields must have an associated
validator.

tip

You can also submit additional fields from the comfort of your browser using
tools like the Firefox Web Developer Toolbar.

You can bypass this security measure by setting the allow_extra_fields option
to true:

The test must now pass but the token value has been filtered out of the
values. So, you are still not able to bypass the security measure. But if you
really want the value, set the filter_extra_fields option to false:

$this->validatorSchema->setOption('filter_extra_fields', false);

note

The tests written in this section are only for demonstration purpose. You
can now remove them from the Jobeet project as tests do not need to validate
symfony features.

During day 1, you learned the generate:app task created a secured application
by default.

First, it enabled the protection against XSS. It means that all variables used
in templates are escaped by default. If you try to submit a job description with
some HTML tags inside, you will notice that when symfony renders the job page,
the HTML tags from the description are not interpreted, but rendered as plain
text.

Then, it enabled the CSRF protection. When a CSRF token is set, all forms embed
a _csrf_token hidden field.

tip

The escaping strategy and the CSRF secret can be changed at any time by
editing the apps/frontend/config/settings.yml configuration
file. As for the databases.yml file, the settings are configurable by
environment:

Even if symfony is a web framework, it comes with a command line
tool. You have already used it to create the default directory structure of the
project and the application, but also to generate various files for the model.
Adding a new task is quite easy as the tools used by the symfony command
line are packaged in a framework.

When a user creates a job, he must activate it to put it online. But if not, the
database will grow with stale jobs. Let's create a task that remove stale jobs
from the database. This task will have to be run regularly in a cron job.

The doDelete() method removes database records matching the given Criteria
object. It can also takes an array of primary keys.

note

The symfony tasks behave nicely with their environment as they return a
value according to the success of the task. You can force a return value
by returning an integer explicitly at the end of the task.

Testing is at the heart of the symfony philosophy and tools. Today, we have
learned again how to leverage symfony tools to make the development process
easier, faster, and more important, safer.

The symfony form framework provides much more than just widgets and validators:
it gives you a simple way to test your forms and ensure that your forms are
secure by default.

Our tour of great symfony features do not end here. Tomorrow, we will create the
backend application for Jobeet. Creating a backend interface is a must for most
web projects, and Jobeet is no different. But how will we be able to develop
such an interface in just one hour? Simple, we will use the symfony admin
generator framework.