Monday, 27 August 2012

App Settings are for Variables that Change with the Environment

My opinion is that app settings (or web.config settings) are generally overused. It is usually reasonable for developers to expect that certain values in their code will change in the future. The temptation is to capture all these variables in the app settings. Doing so however, is a mistake.

The only values that belong in application settings are things that will change with the environment, i.e. TEST, STAGE, PRODUCTION.

Everything else, including the things we think (or even know) will change belong directly in source code. I've learned this rule only over time, and I think its reinforced by the YAGNI principle.


Explanation

Sometimes business requirements are delivered in such a way that they barely need translating into a model or domain or even into some syntax. They will usually be very short user stories, and not scored very highly. Examples include:
"As a patient I want a reminder 1 hour before my appointment." 
"As a broker I want the message push interval to be 10 seconds." 
"As a user I want the warning message to read: 'Are you sure you want to delete?'"
The important thing about these stories to notice is the precision they provide. Product owners like precision. They like to know exactly what their product will or won't do. Developers however, like abstraction. They like removing precision and extracting the general rules behind the process. Developers see a rule like '... a reminder 1 hour before my appointment' and take from it the underlying purpose of the story: 'something must happen at a certain time before something else happens'.

There are clearly a number of ways in which sending the reminder could be implemented, depending on the delivery mechanism or format that I won't discuss. But what is clear is that our Product Owner wants the reminder sent 1 hour prior to the appointment. Most developers would look at this requirement and expect it to change. The one certainty about any piece of software is that it will need to change.

The temptation therefore is that the 1 hour (or 60 minutes) should not be hard-coded - if the value is going to change, then lets keep it somewhere where it's easy to change like the app settings, right? Wrong.

There are two reasons for this: Unit Tests and Continuous Delivery.


Unit Tests

The user story in question is business value waiting to be delivered. We want to be absolutely sure that we are delivering that value. If we ship code that doesn't have a passing unit test titled "A_Reminder_Is_Sent_60_minutes_before_appointment" then we haven't succeeded. (Or perhaps closer resembling conceivable objects something like "Reminder_Delivery_Time_Is_60_minutes_before_appointment_time"). We should be sure that no other changes we might make in future will interfere with this piece of logic.

You might argue that we could write a test that reads this value out of our app settings. But then what you would have is an integration test. The reason we want to capture this behaviour in a unit test is to get the answer we want in the shortest feedback loop possible. Also, we don't want to have to maintain a duplicate app settings file in our unit test project.


Continuous Delivery

The practice of automated deployment is becoming more common. As Scott Hanselman says, "If You're Using XCOPY, You're Doing it Wrong. I would take this further and say that if you are still RDC'ing onto the live server to edit a config setting (like changing 60 minute reminders to 120 minute reminders) then you're also doing it wrong. This means the ONLY way to deploy new code (or changed code) is to use an automatic task, and never tamper with the live servers. In a lot of cases developers do not even have access to the live servers.

So now that our app settings file is no longer accessible, it is certainly not easier to change. If we want to change our reminders from 1 hour to 2 hours we have to do so locally, commit and deploy.

Not forgetting our unit test of course - if we made our change in code and ran our tests we should expect a failure. This is important. We have changed the rules of the system and should expect to have to update our unit test accordingly. The new rules are then captured and protect our logic against future unexpected changes.


Environments

So when are App Settings useful? The purpose for app settings in my opinion is for values that change with the target environment. Connection strings are a perfect example of this. In a TEST environment we would expect to talk to a different database than a PRODUCTION environment.

This is where app settings and Config Transformations come in very handy. One rule to decide is this: If your app setting is the same in every target environment, then it doesn't need to be an app setting.


What about Code Reuse?

Keeping values in one place is very useful. But it doesn't mean they belong in the app settings. Globally accessible static classes with Constant field variables is a perfectly acceptable way of maintaining consistency and adhering to the DRY principle


When App Settings become a Feature

If we begin to update changing values on a regular basis then the overhead of updating unit tests and deploying can become a burden. At this point we have learned that the ability to update such settings needs to become part of the application. A discussion should be had with the Product Owner over the time spent redeploying versus time invested in providing a new feature to allow users to update values without requiring developer time.

Conclusion

App settings are a tempting place to put variables that we expect to change. However, it is more valuable to 'hard code' such values. This is so that we can make valid assertions against the business rules 'at present'. It is in our interest to break a test if this value changes, and know about it as soon as possible. We also know that we should be deploying changes exactly the same way - whether the changes are in the app settings or source code. This means a change to the app settings file is no more difficult or time consuming than it is to change the source code of our system. Target environment variables are the only valid use-case for app settings. This is to allow such values to vary (transform) between environments. App settings should only exist if the value changes between environments.

Monday, 6 August 2012

Specflow BDD test with HttpClient

I've been using Specflow for a while now for various BDD testing scenarios. However until now I'd not come across an example that I'd like to blog about.

Enter: HttpClient and HttpRequestMessage included in .net 4.5 under the System.Net.Http namespace.


Scenario

The story starts with the requirement to drop a client cookie so we know which users have visited the site before. And perform some basic logging about the visit details, query string information, referrer etc.

Straight forward enough, but the complexity arises because we want a single Visit Tracking Utility that spans multiple sites, each on a different platform and written in different languages. Some are even 3rd party sites, so we have minimal control over the html displayed - and zero control over whats going on at the server. In order to achieve this platform-agnostic solution we need to solve the problem with javascript as we accept will have at least this much control over each target site. We also have some specification about the name and value of the cookie as well as the expiry date date.

The technical requirement therefore is a url available to make requests to that will drop a cookie on the clients machine called VisitId, with a Guid value that expires in 7 days time.


The BDD Test

If you are not already, please familiarise with specflow. The plan here is to capture the Behaviour in a Feature file that we can make assertions from based on our code. We begin by citing the scenario we want to test using the Gherkin Given; When; Then format. Specflow then allows us to attach code to the steps we dictate, then we insert the required code and necessary NUnit assertions to validate our scenario.

Starting with a new IntegrationTest project in the solution we add a new VisitTracking feature file and enter the following scernario.

Feature: Visit Tracking
    In order to know which users have previously visited
    I want to drop a client cookie on each visit

Scenario: Setting client cookie for a new visit
    Given the api uri is local.trackmyvisit.com/api/trackvisit
    And the expected cookie name is VisitId
    When I hit the visit tracking uri
    Then the response HttpCode is OK
    And the response sets a cookie
    And the cookie name is correct
    And the cookie value is a valid Guid
    And the cookie expiry is 7 days from now

Each 'Then' and following 'And' gives a chance to make an assertion about the request we have made. We are testing exactly the things described in our specification.

If we build and try to run this now, then specflow will tell us that we are missing the steps specified in the scenario:

-> No matching step definition found for the step. Use the following code to create one:
[When(@"I hit the visit tracking uri")]
public void WhenIHitTheVisitTrackingUri()
{
    ScenarioContext.Current.Pending();
}

Copy from the test output all the necessary steps into a VisitTrackingSteps class. With a couple of extra helper methods and paramter arguments on your class should look like the following. Don't forget to use the [Binding] attribute before the class declaration or specflow won't know where to look for the steps. Also you will need to install the System.Net.Http package to get reference to the HttpClient object et al.

[Binding]
public class VisitTrackingSteps
{
    private string _uri;
    private string _cookieName;
    private HttpResponseMessage _result;

    [Given(@"the api uri is (.*)")]
    public void GivenTheApiUriIs(string uri)
    {
        _uri = "http://" + uri;
    }

    [Given(@"the expected cookie name is (.*)")]
    public void GivenTheExpectedCookieNameIs(string cookieName)
    {
        _cookieName = cookieName;
    }

    [When(@"I hit the visit tracking uri")]
    public void WhenIHitTheVisitTrackingUri()
    {
        var client = new HttpClient();
        var msg = new HttpRequestMessage(HttpMethod.Get, _uri);

        _result = client.SendAsync(msg).Result;
    }

    [Then(@"the response HttpCode is (.*)")]
    public void ThenTheResponseHttpCodeIs(HttpStatusCode statusCode)
    {
        Assert.That(_result.StatusCode, Is.EqualTo(statusCode));
    }

    [Then(@"the response sets a cookie")]
    public void ThenTheResponseSetsACookie()
    {
        var isSetCookieHeaderPresent =
            !String.IsNullOrEmpty(GetValueOfSetCookieHeader());
        Assert.IsTrue(isSetCookieHeaderPresent);
    }

    [Then(@"the cookie name is correct")]
    public void ThenTheCookieNameIsCorrect()
    {
        var firstKey = ParseSetCookieValue().AllKeys.FirstOrDefault();
        Assert.That(firstKey, Is.EqualTo(_cookieName));
    }

    [Then(@"the cookie value is a valid Guid")]
    public void ThenTheCookieValueIsAValidGuid()
    {
        Guid guid;
        var guidValue = ParseSetCookieValue()[_cookieName];
        var isValidGuid = Guid.TryParse(guidValue, out guid);

        Assert.IsTrue(isValidGuid);
    }

    [Then(@"the cookie expiry is (.*) days from now")]
    public void ThenTheCookieExpiryIsDaysFromNow(int days)
    {
        var expiresValue = ParseSetCookieValue()["expires"];
        var expires = Convert.ToDateTime(expiresValue);
        var rangeStart = DateTime.Now.AddDays(7).AddMinutes(-1);
        var rangeEnd = DateTime.Now.AddDays(7).AddMinutes(1);

        Assert.That(expires, Is.InRange(rangeStart, rangeEnd));
    }

    private string GetValueOfSetCookieHeader()
    {
        IEnumerable<string> values;
        _result.Headers.TryGetValues("Set-Cookie", out values);

        return values.FirstOrDefault();
    }

    private NameValueCollection ParseSetCookieValue()
    {
        var collection = new NameValueCollection();
        var cookieValArray = GetValueOfSetCookieHeader().Split(';');

        foreach (var arr in cookieValArray.Select(s => s.Split('=')))
        {
            collection.Add(arr[0].Trim(), arr[1].Trim());
        }

        return collection;
    }
}

You can see that in the 'When' method the HttpClient and HttpRequestMessage are used to initiate the call to the visit tracking site - at present not yet created. The SendAsync method of the HttpClient is called using a GET request to the path of the api controller method. This is replicating exactly what the script tag will do from each web page it appears on. So we have been able to quickly conjure up a http request without the use of selenium or other web driver. The other nice part is that we can interrogate the response from the request by accessing the Result property of the generic Task we get from calling SendASync.

The tests will build and run now but there is currently nothing to hit so the tests will fail. We can now implement the logic that will fulfil the spec.


Tracking Logic using an Api Controller

Our spec tells us we need a uri we can hit that will perform the visit tracking logic and write back a cookie to the client through the http response. Furthermore, as there will be now javascript or html returned in the response (i.e. nothing for the client to parse) this sounds like a candidate for using an MVC4 WebApi Controller. The controller implements the exact requirements of the spec.

public class TrackVisitController : ApiController
{
    // call using GET method to /api/trackvisit
        
    public HttpResponseMessage Get()
    {
        var cookie = new HttpCookie("VisitId")
        {
            Value = Guid.NewGuid().ToString(),
            Expires = DateTime.Now.AddDays(7)
        };

        HttpContext.Current.Response.Cookies.Add(cookie);
            
        return new HttpResponseMessage(HttpStatusCode.OK);
    }
}

The unit tests are already fathomable for this method, but this post is about integration testing, so thats what we'll focus on,


Script Tag

We then only need to tag each page we wish to track. The uri is used for the source of a script tag on each page we want our tracking system to work on.

<script src="http://test.trackmyvisit.com/api/trackvisit" />

All the source is available in my GitHub Repository. And you will have to setup the site locally and adjust the host binding yourself if you want this to run out-of-the-box :-).