weeks trying to increase the coverage, and then give up at around 20% with no notice- able improvement in quality or maintainability. (I also saw a team inherit a large C# codebase with no tests, and 18 months later they had achieved their goal of 80% cov- erage, so there are exceptions to every rule!)
The problem with setting an arbitrary goal of improved test coverage is that you’ll start by writing the easiest tests first. In other words, you’ll write dozens of tests for
Code that happens to be easily testable, neglecting more important but less test- able parts of the codebase
Code that is well written and easy to reason about, even though a code review might be enough to verify that this code works as expected
AUTOMATEALLYOURTESTS
Most developers would agree that unit tests should be fully automated, but the level of automation for other kinds of tests (such as integration tests) is often much lower. Although we want to run these tests as often as possible when refactoring, in order to spot regressions quickly, we can’t do so if they rely on manual labor. Even if we had an army of willing testers to rerun the whole integration test suite every time we made a commit, it’s possible that they would forget to run a test, or misinterpret the results. Plus it would slow down the development cycle. Ideally we want all of our regression tests, not just the unit tests, to be 100% automated.
One area that cries out for automation is UI testing. Whether you’re testing a desk- top application, a website, or a smartphone app, there’s a huge selection of tools avail- able to help you automate your tests. For example, tools such as Selenium and Capybara make it easy to write automated web UI tests. The following code sample shows a Capybara script that you could use to test World of RuneQuest’s player profile page, which you saw earlier in the chapter. This simple Ruby script opens a web browser, logs in to World of RuneQuest, opens the My Profile page, and checks that it contains the correct content, all within a matter of seconds.
require "rspec" require "capybara" require "capybara/dsl" require "capybara/rspec" Capybara.default_driver = :selenium Capybara.app_host = "http://localhost:8080" describe "My Profile page", :type => :feature do it "contains character's name and species" do visit "/"
Login as a known test user
fill_in "Username", :with => "test123" fill_in "Password", :with => "password123" click_button 'Login'
visit "/profile"
Opens the 'My Profile' page and checks its content
expect(find("#playername")).to have_content "Test User 123" expect(find("#speciesname")).to have_content "orc"
end end
This test can easily be run by a developer on their local machine or by a CI server such as Jenkins. It could also be configured to run in headless mode instead of opening and manipulating a web browser, in order to speed up the test execution.
Of course, it’s not possible to test everything in your application using UI tests alone, but they make a valuable addition to your test suite, especially when working with legacy code that may be difficult to test by other means.
4.3.3 Make the users work for you
You’ve done pair programming, you’ve conducted code reviews, you’ve run your unit tests, functional tests, integration tests, system tests, UI tests, performance tests, load tests, smoke tests, fuzz tests, wobble tests (OK, I made that last one up), and they’ve all passed. This means your software has no bugs, right?
No, of course not! No matter how much testing you do, there will always be a pat- tern you haven’t managed to check. Every test that you run before release is, in a sense, an attempt to emulate the actions of a typical user, based on your best guess about how users will use the software. But the quality and rigor of this simulacrum will never match the real thing—the users themselves. So why not put this army of unwit- ting testers to good use?
You can make use of user data to help ensure the quality of your software in a few ways.
Perform gradual rollouts of new releases, while monitoring for errors and regressions. If you start to see unusually high error counts, you can stop the rollout, investigate the cause, and either roll back to the previous version or fix the problem before continuing the release. Of course, error monitoring and subsequent rollback can be automated. Google is one company known for its dedication to gradual rollouts, with major releases of Android taking many weeks to reach all devices. Gather real user data and use it to make your tests more productive. When load testing
a web application, it’s difficult to generate traffic that reflects real usage pat- terns, so why not record the traffic of a few real users and feed that into your test scripts?
Perform stealth releases of new versions, whereby software is released into the production environment but not yet visible to the users. All traffic is sent to both the old and new versions, so you can see how the new version works against real user data.
4.4
Summary
Successful refactoring takes discipline. Perform refactorings in a structured way and avoid combining them with other work.
Removal of stale code and low-quality tests is a nice way to get the refactoring ball rolling.
Use of null pointers is a very common source of bugs, no matter what language you’re using.
37 Summary
Prefer immutable state over mutable.
Use standard design patterns to separate business logic from implementation details or to make complex business logic more manageable and composable. Use the View Adapter pattern to keep complex logic out of your application’s
view layer.
Beware any class or module with Util in the name.
Introduce a layer of indirection in order to inject mock dependencies in tests. Unit tests aren’t a silver bullet. You need tests at multiple levels of abstraction to
protect against regressions caused by refactoring.
As a developer, you may inherit projects built on exist- ing codebases with design patterns, usage assumptions, infrastructure, and tooling from another time and ano- ther team. Fortunately, there are ways to breathe new life into legacy projects so you can maintain, improve, and scale them without fighting their limitations.
Re-Engineering Legacy Software is an experience- driven guide to revitalizing inherited projects. It covers refactoring, quality metrics, toolchain and workflow, continuous integration, infrastructure automation, and organizational culture. You’ll learn techniques for introducing dependency injection for code modular- ity, quantitatively measuring quality, and automating infrastructure. You’ll also develop practical processes for deciding whether to rewrite or refactor, organizing teams, and convincing management that quality matters. Core topics include deci- phering and modularizing awkward code structures, integrating and automating tests, replacing outdated build systems, and using tools like Vagrant and Ansible for infra- structure automation.
What's inside:
Refactoring legacy codebases
Continuous inspection and integration Automating legacy infrastructure New tests for old code
Modularizing monolithic projects
This book is written for developers and team leads comfortable with an OO language like Java or C#.