Adapted from the “I’m scared to change that” talk given in the SixPivot Community Mentoring Program. Please enjoy and feel free to use resources links at the bottom.
Modifying big, complex, or unfamiliar software can be a daunting task, fraught with peril. We’re going to go over some testing methods and tooling options to help you gain confidence in making changes to complex or unfamiliar software. We’ll be covering:
Automated tests and test selection
Understanding and improving code
This article is aimed at beginner-to-intermediate developers, but serves as a good refresher for anyone.
Unit Tests are Good
We all “know” that Unit tests are good (for our purposes, let’s say ‘Unit Test’ covers all code-based tests). So that we’re on the same page, here’s a quick recap of why:
Repeatable – they can be run the same way over and over again
‘Cheaper’ than manual testing – running is quick and cheap. You ‘lose’ time writing them, but you’re also writing and executing fewer manual test plans
Automatable (apparently that’s a word) * – they can be run automatically by tools
Finds bugs early – the act of writing tests often reveals bugs in code that people think have no issues
‘Regression’ (ensuring changes do not break existing functionality) – since they are cheap and repeatable to run, they can be used to ensure changes don’t break existing functionality
Trackable * – when run automatically by tools, test counts, pass rates, breakages, execution times can all be tracked and improved
Makes integrating changes easier/safer * – again, having tools run tests at stages such as commit and Pull Request ensures changes integrate well with other changes
A form of ‘Code Documentation’ – other developers can read and understand the intent of the code
(*) These benefits are gained when the running of the tests is automated in a build tool, such as DevOps, TeamCity or Jenkins
Types of Automated Tests
You may have seen Testing Pyramids before. Every source has their own take on what the levels are.
While there are a variety of ideas across different authors, they all share some common ideas
The ‘foundation’ of your testing triangle is made up of the cheapest, fine-grain, minimal-responsibility, fastest tests you create, whereas the top portion tests more components in a more complex, often slower, way.
The lower portions of the triangle are cheaper and quicker to create and run, and the higher portions are more ‘expensive’
Important Forms of Automated Testing
It’s important to be aware of the different forms of automated tests. There is some overlap between types of tests, and many sources have slightly different classifications, however the following list should cover the majority of cases.
Unit Tests Test ‘units’ – single functions and how they act upon inputs and dependencies. Dependencies are mocked
Approval Tests Approve and save the output of a test to compare against future test runs
Component Tests Several classes (or the whole app) work in concert with the ‘boundaries’ mocked
Acceptance Tests (in this context, Behaviour tests) A form of unit and/or component test that asserts scenarios
Integration Tests Tests that act on the system while it is connected to the downstream systems, such as Databases, APIs, Queues etc
UI Tests Automated tests that drive a Web Browser to interact with the application, e.g. Selenium or Cypress
Performance Tests Test Suites that run large workloads against large datasets to help minimise performance degradation
Smoke Tests A subset of UI, Integration, or manual tests that give quick confidence that the system as a whole works correctly
Manual Tests A person manually performs test types such as Integration, UI, or Smoke tests
An Aside – TDD or No TDD?
Test Driven Development is the concept of writing tests first, and then implementing your method/component to successfully pass the tests.
This method of testing helps many people design code that is testable and has good test coverage
However, you do not need to follow TDD practices as long as the code you produce is testable. How?
What if there are little-to-no tests?
Your confidence in change will be proportional to the effort invested in validating the change
But what if we need to make a change and there are no tests?
If you aren’t experienced with this application, familiarise yourself with the application and the area you will be modifying
For unfamiliar codebases, do not attempt to understand all parts in detail. Get an overall ‘feel’ and understanding of the application, then drill down. This should include spending time with experts or users where possible. Talking to people can help you come to a better-shared understanding of the domain and its intent.
Understand the Scope of your change
Investigate and note the implications of your change on the wider application
Tooling will help you understand the implications of a change – parts of the system this change will affect. E.g. ‘Find Usages’ or even just ‘Search’
Write some tests to assert the current functionality
Write tests at a ‘level’ that has the least friction. E.g. if the code isn’t very ‘testable’, consider that Component Tests might be the most suitable type of test to start with
IMPORTANT – If you do not create these tests first, if/when you find an error, you may not know if it was existing or introduced
This step may require refactoring. If so, try to maintain the original algorithms as much as possible
Perform your change
Try to reduce Code Smells as you go. Follow the ‘Scout’s Rule’ of always leaving things better than when you found them
Ensure the tests are updated to assert the correctness of the change (before or after making the change, TDD or not)
Expand your tests further as much as you are able before moving on from the task
Take your new-found test glory as far as you can within the bounds of your team charter/project/agreements
Here is a quick list of examples of tools that can assist in the discussed strategies:
Unit Test Libraries
Code libraries to write unit tests with
NUnit, XUnit, Jasmine, Karma etc. Also ‘Mocking’ libraries such as NSubstitute or Jest
UI Test Libraries
Selenium, Cypress etc
Build Tools (CI/CD)
Automatically run tests on Commit, on Pull-Request (code integration) and Merge.
Can also be used as a platform for running Integration, UI and Performance tests continually, or on a schedule
Azure DevOps, Jenkins, TeamCity
IDE / Code Editors
Robust, modern editors have a plethora of helpful tools for understanding the implications of your changes, such as
Code Navigation – symbol usage analysis, static analysis (capturing invalid or potentially problematic code).
Refactoring – Resharper, in particular, has strong assistance for refactoring and detecting ‘smells’ E.g. Visual Studio, VS Code, Rider, Resharper
To gain confidence in change:
Have good coverage of a variety of test types that assert your system and its components are working as expected
Ensure the tests are running automatically and consistently
When a system is poorly tested, start testing the most frictionless way possible to gain momentum
When changing untested components, start by writing tests that assert the current understanding of the functionality, then make your change
Use tooling to help you understand systems and dependencies
Understand the system you are changing from the point of view of understanding the implications of change
Gaining confidence in changes not only reduces the stress of shipping changes, but also reduces overall work by minimising bugs, and easing the weight of required processes around change.