Automation Road Blocks: Altering the Behavior of an ASP.NET Server for Tests

switching

Any realistic application is hard to test. Often much of the complexity is unavoidable, but sometimes things are harder than they need to be.

The main component of my current project is a fairly simple backend server in ASP.NET MVC, but almost all of the database access occurs through a separate support service with which our backend interfaces.

As you might imagine, system testing this project is really difficult. Basically the best we can do is to test against a faked-out version of the support service. Our system tests communicate with the app via HTTP, so the overall system ecosystem looks like this:

ASP.NET server with fake service

The Problem

Debugging an IIS server with the Run button in Visual Studio is a bit different than when you hit Run on a normal app. For an IIS server app, instead of running directly it attaches to your dll within the IIS Express process. Because of this we cannot have our test code instruct our app to look at the fake support service we made. Instead we need make sure our built debug dll is looking at a different endpoint whenever we want to run tests.

At first we managed switching between the real and fake services by manually editing the location in Web.config before running system tests. This was a major hassle and very error prone. Not only did the file need to be edited to run tests, you also had to rebuild and run the server in order for the test runner to pick up the changes. Unable to stand this process any longer, I set about automating the changes for tests.

The Solution

To automate system test running, I needed to ensure three things:

  1. The ASP.NET server dll deployed to IIS Express will look for the service endpoint where our fake support service runs.
  2. The fake support service is running.
  3. IIS Express is running (not necessary the case if I haven’t hit Run yet in Visual Studio).

Automating each of these requirements turned out to be pretty tricky, but it was well worth the time.

1. Deploying the ASP.NET server in “Test” mode

I had a pretty good hunch that using a “Test” Build Configuration would be the best way to easily alter the behavior of the app for running our system tests. You can’t use Web.config overrides (e.g. Web.Test.config) for this because they don’t apply in Debug mode. So instead I added extra “TestApi” variables to Web.config, and made their use conditional to configuration:

public static void SetupContext()
{
#if TEST
    const string api = "TestApi";
#else
    const string api = "Api";
#endif
    var supportServiceRoot = "http://" + ConfigurationManager.AppSettings[api + ":Host"] + ":" +
        ConfigurationManager.AppSettings[api + ":Port"] + ConfigurationManager.AppSettings[api + ":BaseAddress"];
// ...
}

Using a compiler directive is far from my preferred method of altering behavior, but at least the messiness was contained to one spot.

Then I needed to make sure the Test configuration was the last one built when tests start, so our system tests would find the right endpoint. Thankfully, SpecFlow lets you run code at the start of a test suite (essentially a “before all”). So in my “before all” I shelled out and used a Rake task that calls MSBuild with the desired settings.

[BeforeTestRun]
private static void EnsureReadyForTests()
{
    var process = new Process
    {
        StartInfo = new ProcessStartInfo
        {
            FileName = "bundle.bat",
            Arguments = "exec rake test_prep"
        }
    };
    process.Start();
    process.WaitForExit();
}

Of course you could also open a normal Windows command shell or a powershell.

2. Ensuring the fake support service was running

Additionally, I needed to make sure the fake support service was running so our tests could hit it. I added a Rake task that checked against its port, and if it isn’t running it launches a new command shell and starts the server.

def is_port_open?(ip, port)
  begin
    Timeout::timeout(20) do
      begin
        s = TCPSocket.new(ip, port)
        s.close
        return true
      rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
        return false
      end
    end
  rescue Timeout::Error
  end
 
  return false
end
@echo off
cd ..\fake-service
set BUNDLE_GEMFILE=
start cmd /k bundle exec rake

I started the service in its own shell, so it doesn’t prevent my test_prep rake task from completing before tests begin.

3. Ensuring IIS Express is running

Making sure IIS Express is running was surprisingly tricky. Just running iisexpress.exe didn’t work when as part of a Rake task (and presumably not as part of a batch script either). I had to launch it via a start command:

start "" "C:\Program Files (x86)\IIS Express\iisexpress.exe" /Site:MyProjectName

For convenience sake, I also created a batch script to shut down IIS Express. It involves a powershell script file, and it is so complicated that I never would have bothered I to write it, had I not been able to lean on the AO braintrust. Turns out that someone had done almost the same thing. Here’s the script for reference:

foreach($proc in (ps | Where { $_.name -like "iisexpress"} )) {
  $iisid = $proc.Id
  $iis = Get-WmiObject Win32_Process -filter "ProcessId=$iisid" | Where-Object { $_.CommandLine -like "*MyProjectName*" } | Select-Object -first 1
  if($iis) {
    stop-process -force $iis.ProcessId
  }
}

Mastering Your Environment

With these pieces in place and running at the start of system tests, I no longer have to worry about which configuration I have been using or what settings need to be changed before I run our tests. There are still parts of our project I wish were easier to test, but these changes made development a fair bit easier.

I can think of few things more necessary to effective development than being in control of your development environment. That means not only knowing how to perform required tasks, but being able to automate them. I am not as critical of the Windows ecosystem as some, but one of my main complaints is the difficulty of automation.

Automation is the secret sauce for development. I think of tasks I have automated has having delegated the work to my smartest possible self. Once my smartest self figures out something complicated, he performs that task quickly and flawlessly over and over again. Regular me is apt to forget things or make tiny mistakes. So, development tool makers, I ask you: let me automate. Let me be my smartest self all the time.