In this chapter, we tackle adding validation and business rules to our blog application. As always, we begin with a set of stories (see Figure 19.1).
We need to implement the following stories:
• If a user submits a blog entry without a title, the validation error message “Title Is Required” displays.
• If a user submits a blog entry with a title longer than 500 characters, the validation error message “Title Is Too Long” displays.
• If a user submits a blog entry without text, the validation error message “Text Is Required” displays.
• Blog entry names should be encoded in an easy-to-understand way automatically. For example, the blog entry name “My Summer Vacation” should be encoded "My-Summer-Vacation"
.
• If a user submits a blog entry with a title but not a name, the name is retrieved from the title.
First, we want to prevent a user from a submitting a blog entry when the user does not include a title for the blog entry. We can capture this requirement with the test in Listing 19.1.
The test in Listing 19.1 creates a blog entry without a title and invokes the Blog
controller Create()
action. If the model state does not include the specific error “Title Is Required,” the test fails.
Notice that the test makes use of a utility method named HasErrorMessage()
that iterates through all the error messages in model state to perform a match. The HasErrorMessage()
method is also included in Listing 19.1.
If you run this new test, it fails (see Figure 19.2). The test fails because we have not yet implemented any validation logic. Now that we have a failing test, we can modify the application code for our blog application.
The simplest way to get the test in Listing 19.1 to pass is to add the required validation logic directly to the Create()
action of the Blog
controller. The modified Create()
action in Listing 19.2 includes a new validation section that verifies that the Title
property is not empty.
After you modify the Blog
controller, our new test passes (see Figure 19.3). Time to celebrate? Unfortunately, if you look again at Figure 19.3, you notice that several tests that previously passed are now failing. We have introduced a change that broke other code in our application.
In this particular case, there is nothing wrong with our application code. The problem is with our existing test code. None of the tests that we previously wrote passed a value for the Title
property. Because we just made the Title
property required, all these existing tests now fail.
For example, Listing 19.3 contains the IndexReturnsBlogEntriesByYear()
test. Before we made the Title
property into a required property, this test passed. Now, this test fails because when the test creates new blog entries, the test does not create a value for the Title
property.
An easy fix to this problem would be to simply modify all our tests so that they include the required blog entry Title
property. However, this approach to solving the problem is not a good solution. The next time that we introduce a new property or modify an existing property, we would need to modify all our tests again.
The real problem is that our test code is too brittle. We need to consider how we can make our test code more resilient to change. In other words, this is a good time to consider how we can refactor our test code to improve its design.
You need to tend to the design of your test code as much as you tend to the design of your application code. In this section, we improve our test code so that it is more resilient to change.
The problem with our test code right now is that we create blog entries everywhere. We need to create a single location in our code where we create our blog entries (a single point of failure). That way, if we need to change the properties of the blog entry
class, we don’t need to make that change everywhere.
Listing 19.4 contains a new class named BlogEntryFactory
. This class exposes a static method named Get()
that returns a valid blog entry with all its properties in a valid state. In addition, the BlogEntryFactory
class exposes a GetWithDatePublished()
method that enables you to easily return a blog entry with a particular publication date.
Now that we have a BlogEntryFactory
class, we can use this class within our test code to create our blog entries. For example, the test in Listing 19.5 has been refactored to use the BlogEntryFactory
class GetWithDatePublished()
method.
After the test code is updated, all the tests run successfully (see Figure 19.4).
According to the next user story in our list, we need to verify that a user cannot submit a blog entry Title that contains more than 500 characters. This story will be easy to implement.
First, we create the test in Listing 19.6. This test creates a blog entry with a title that is too long and passes the blog entry to the Blog
controller Create()
method. The test verifies that the validation error message “Title Is Too Long” is added to model state.
If you run the test in Listing 19.6, the test fails (see Figure 19.5). We want the test to fail so that we can allow ourselves to write new application code.
The Blog
controller Create()
action in Listing 19.7 has been updated with the necessary code to pass the test. The modified Create()
action adds an error to model state when the Title
property is longer than 500 characters. At this point, if you run the tests again, we are back to green.
Every once in a while, it is a good idea to actually run the ASP.NET MVC application that you build and view it in a web browser. The final goal, after all, is to create a working blog application.
If you run the blog application—by selecting the menu option Debug, Start Debugging or pressing F5—and you click the Create New link, you get the form for creating a new blog entry. If you attempt to submit the form without entering any values in the form fields, you get the validation error messages illustrated in Figure 19.6.
Notice that there are three validation error messages. We can account for the error associated with the Title
property. We modified the Blog
controller to add this error to model state when the Title
property is empty.
However, there are two “A Value Is Required” errors. Where do these errors come from?
We can easily account for one of the “A Value Is Required.” errors. One of these errors corresponds to the DatePublished
property. The DatePublished
property is a DateTime
property, and a DateTime
property cannot accept an empty value. Therefore, the ASP.NET MVC framework adds an error message to model state automatically. (In particular, the default model binder adds this error message to model state.)
But, there is still one validation error message left. This last error is mysterious because it does not correspond to any of the fields in the HTML form.
The final validation error message corresponds to the blog entry Id
property. The Id
property is an integer
property. Like the DatePublished
property, you cannot assign an empty value to the Id
property.
However, we want the ASP.NET MVC framework to ignore the Id
property because this property gets its value from the database. The Id
property corresponds to an Identity column in the database.
To make the validation error message go away, we need to modify the Create()
action so that it ignores the Id
property when creating an instance of a blog entry from the HTML form fields. A modified Create()
action is contained in Listing 19.8.
Notice that the blog entry
parameter in Listing 19.8 is decorated with a Bind
attribute. This attribute excludes the Id
property from the set of properties bound to the HTML form fields submitted to the server.
Our validation code works, but it is not pretty. We are violating the Single Responsibility Principle (SRP) by mixing our validation logic into our controller logic. In general, you want each layer of your application to assume a single responsibility.
The software design principles exist for a reason. Mixing together different types of logic makes an application more difficult to maintain and modify over time. We need to clean up our code. A place for everything and everything in its place.
In this section, we refactor our code to migrate our validation logic to a separate service layer. We can fearlessly refactor our application to improve its design because our application is well covered by tests. The tests act as our safety net for change.
Listing 19.9 contains a new blog service
class that acts as our service layer. This class contains all the validation logic that was previously located in the blog controller
class.
The CreateBlogEntry()
method validates the properties of the blog entry passed to the method. If there are any validation errors, the method returns the value false
. Otherwise, the CreateBlogEntry()
method calls the CreateBlogEntry()
method on the blog
repository class to add the new blog entry to the database and returns the value true
.
We need to modify the Blog
controller and Archive
controller to use the blog service instead of the blog repository. The modified Blog
controller is contained in Listing 19.10.
Notice that an instance of the blog service is created in the blog
controller constructors. Previously, the blog
controller interacted directly with the blog
repository. The new version of the blog
controller interacts with the blog service and the blog service interacts with the blog
repository.
These changes provide us with a clean separation of concerns. Our controller logic is kept in the controller layer, our validation logic is kept in our service layer, and our database logic is kept in our repository layer.
Because we have the safety net of our tests, we can refactor fearlessly. After these massive changes to the architecture of our application, our original tests continue to pass (see Figure 19.7).
Before we can end this chapter, there are two final stories that we need to implement. Both of these stories relate to business rules concerning a blog entry name:
• Blog entry names should be encoded in an easy-to-understand way automatically. For example, the blog entry name “My Summer Vacation” should be encoded like "My-Summer-Vacation"
.
• If a user submits a blog entry with a title but not a name, the name is retrieved from the title.
What’s the difference between the title and name of a blog entry? The name of a blog entry is used when retrieving a blog entry. For example, you can retrieve a blog entry named “My Summer Vacation” by requesting the following URL:
www.MyBlog.com/2010/12/25/My-Summer-Vacation.
The title of a blog entry, on the other hand, is the title that is displayed for the blog entry in a page and in the RSS feed. For example, My Summer Vacation.
A blog entry name can only contain valid characters for a URL. According to the official standards (RFC 1738), a URL can contains only alphanumeric characters and a limited number of special characters. In particular, a name cannot contain spaces.
Let’s start with a test for the first story. We need to create a test that verifies that any special characters in a blog entry name get encoded automatically. The test in Listing 19.11 uses a regular expression to verify that a set of blog entry names get encoded correctly.
To pass the test in Listing 19.11, we need to modify the blog service so that it encodes the blog entry Name
property. The updated blog service in Listing 19.12 correctly encodes the Name
property.
One last story. If a user supplies a blog entry title, but not a name, we should get the name from the title automatically. We can express this requirement with the test in Listing 19.13.
The test in Listing 19.13 verifies that when a blog entry is created with a title, but not a name, that the name is retrieved from the title. The modified CreateBlogEntry()
method in Listing 19.14 causes this test to pass.
In this chapter, we added validation and business rules to our blog application. We started by adding the validation rules in the simplest way possible; we added the validation rules directly to our controller
class.
After we successfully passed our tests, it was time to refactor our application to have a better design. We refactored our validation code into a separate service layer.
Finally, we implemented two business rules related to blog entry names. We verified that blog entry names are valid. We also implemented a business rule that causes the Name
property to be retrieved from the Title
property when no name is supplied.
18.119.143.17