Disclaimer: This post includes Amazon affiliate links. If you click on one of them and you make a purchase I’ll earn a commission. Please notice your final price is not affected at all by using those links.
Welcome to another post part of the series covering Quality Attributes / Non-Functional Requirements, this time I’m talking about
Maintainability using Linting, Code Coverage and Continuous Integration.
What is Maintainability?
Maintainability indicates how difficult is to modify our system, this means how easy or hard is to add new features, make improvements or fix defects. In Building Evolutionary Architectures there’s a concept called Fitness Functions that allows us to define measurable artifacts used for collecting data we can refer to when making changes over time, with the end goal to make those artifacts part of our building pipeline.
For example a fitness function we can implement is one to collect our service latency, perhaps using OpenTelemetry, this function can then be used as the baseline to compare when deploying new changes, this way we can determine if what was deployed is making our system better or worse according to that concrete metric we collected since the beginning of the service’s lifetime.
When working with Fitness Functions the key is to use Continuous Integration to continuously exercise those rules, this means every time a new change is pushed into a our version control system an automatic process will trigger to run those functions, some well-known Continuous Integration services include Jenkins CI, Gitlab CI, Circle CI and, the one I’m going to be using for this example, Github Actions.
Choosing one in particular depends mostly on your use case, budget and existing third party integrations. All of them provide a way to configure jobs or steps using some sort of configuration file that determines the build process to execute as well as a way to generate artifacts to make available back into your Version Control System, in cases you use Github, Bitbucket or Gitlab.
Adding Fitness Functions to measure maintainability in Go
The code used for this post is available on Github.
The fitness functions I’m implementing in this post are what I think are the minimum required for any professional project: Testing, Code Coverage and Linting.
Regarding Testing, I’ve covered it previously in other posts from different point of views, for example when testing REST APIs, when testing Third Party HTTP Requests and also when doing some Integration Testing using the Repository Pattern.
When working on Go projects I follow the recommendations suggested by the Go team, this specifically involves things like:
- Avoiding Assert Packages, and
- Using Table-Driven Tests
I also keep the number of third party dependencies as small as possible, I rarely use any packages outside of the following list:
- google/go-cmp: for comparing values in tests,
- ory/dockertest: for integration testing using Docker containers,
- dnaeon/go-vcr: for recording and replaying HTTP interactions, and
- h2non/gock: for HTTP traffic mocking and testing.
Regarding Code Coverage, I previously discussed how to generate that artifact using the
test command included in the go toolchain, depending on the Version Control System you use you may need to configure it to read the output so the generated value can be available in your Pull Requests or Commit details, for example in Gitlab CI you can see the covered lines as well as the total percentage overall when configured properly.
For this post I’m using a third party service called Codecov that integrates nicely with Github and displays coverage details in the created pull requests as well as a nice Onion-like diagram that displays the coverage per Go package for the whole project.
Finally regarding Linting, the de-facto tool in Go is golangci-lint, this tool includes a collection of multiple linters, please refer to the configuration I currently have for the project, I usually have the following as the default configuration and I start adding concrete settings per linter or exceptions:
linters: enable-all: true disable: - exhaustivestruct # Should be configured to handle Args-like types. - goerr113 - gofumpt # Prefer `gofmt` rules / some rules conflict with `wsl` # The following are deprecated linters, added to avoid intial warning when running - golint - interfacer - maligned - scopelint
Finally, to connect all those three Fitness Functions what is left is to configure our Continuous Integration service, in this case Github Actions, to do that there are two workflows one of Testing, that also includes Code Coverage, and another one for Linting.
With all of that in place every time we push a new commit to our remote repository both Github Actions workflows will trigger to indicate whether our change improved or worsened the status of our project.
Maintainability is hard to achieve if there is no baseline regarding the status of our codebase, Testing, Code Coverage and Linting should be the minimum required Fitness Functions to implement when creating any new project, those will allow the team in charge to follow the same guidelines and keep the implementation consistent across the board.
If you’re looking to sink your teeth into more Software Architecture-related topics I recommend the following links:
- Quality Attributes / Non-Functional Requirements
- Agile Software Development, Principles, Patterns, and Practices
- Hands-On Software Architecture with Golang
- Building Evolutionary Architectures: Support Constant Change - Post