Quick and Dirty Isn’t Always Bad

Testing code is always a good idea. Everyone knows it, and yet it’s all too common to find experienced developers and high-powered companies with little or no test-coverage. Even teams who start out writing tests often fail to maintain the practice over time. Worse yet, it is not uncommon for robust test suites to become so bloated and inconvenient to run that they are seldom used, defeating the purpose to begin with.

At Depict, the engineering team was running into this exact situation. We had extensive model, controller, and acceptance tests for important functionality and edge cases in our Rails app. However, the acceptance tests in particular were taking a long time to complete. The entire test suite took over 20 minutes to run.

After watching the tests run in Selenium, we noticed that a significant amount of time was being consumed by the sign in process. To sign a dummy user in for each test case, they had to go through the UI portion of the sign in flow. This included a modal with animations and then the call to log the user in. All in all, the process could take up to 5 seconds.

Multiplied across over 100 acceptance tests, that was over 8 minutes of simply logging users in.

With controller tests, it’s easy to log a user in quickly, as the RSpec testing environment provides direct access to the session object. However, with acceptance tests the session object is not available. The team searched for quite some time, attempting all sorts of different approaches, none of which worked.

Ultimately, the idea was proposed of creating a lightweight endpoint whose sole purpose was to set a user as logged in. We set up a very simple TestController and added a route:

in routes.rb

if rails.env == "test"
get "/test/set-active-user/:id", to: "test#set_active_user"
end

in TestController.rb

def set_active_user
session[:user_id] = params[:id]

return render json: {success: true}
end

In this way, the acceptance tests could make a quick GET request to /test/set-active-user/[user_id to log in]. The user is thenlogged in and the test can continue with a logged in user state. Since the route performs one quick action and returns JSON, it returns in a fraction of a second, compared with having to wait for the full login UI flow.

The route is wrapped in a check to ensure that the only time it is available is in a testing environment, preventing a backdoor entry to the system in production. (The testing environments were behind additional authentication walls.)

Clearly, the most elegant way to address the problem would have been to gain access to the session object from the acceptance test itself. However, in lieu of finding a way to do that, this method reduced the time to run the test suite by over 30%. A faster running test suite encouraged the team to run the tests more often, therefore improving code quality and allowing refactoring and iterations to progress more smoothly.

This brings me back to the idea that the “best” solution isn’t always the most practical, and taking the bigger picture into consideration is important. A somewhat hacky solution saved significant time both in building the solution and running the test suite down the line. From a more holistic perspective, this quick and dirty approach was a sizable win for us.

As developers it’s easy to fall into the trap of searching for the most performant, elegant, technically savvy solution. However, it’s important to keep in mind other factors including complexity of implementation, ease of maintaining, and modularity. In our case, while the solution was not the most graceful, it was the most useful and advantageous when considering a wider, more practical range of criteria.