Category: Testing and Quality Assurance

Best practices for testing and ensuring the quality of software.

  • Using Bitbucket Pipelines to Automate Project Releases

    Generating releases for your project shouldn’t be a chore, yet many times it does prove to be a pain. If you don’t release very often or on a regular schedule, you have to go back and remember how to do it. This can result in inconsistencies in your releases, which makes it harder on your users. Read on to learn how you can define steps in your Bitbucket Pipelines to automate your project releases.

    I recently set up automated releases for a C++ project in Bitbucket. This process automates the steps to create a release branch, bump the version per Semantic Versioning, generate a changelog according to Conventional Commits, and push the release back to git, fully tagged and ready to go. Here is how I did it.

    Define the common steps in your pipeline

    # Default image to use - version 3.x
    image: atlassian/default-image:3
    definitions:
      services:
        docker:
          memory: 7128
      steps:
          - step: &Build-Application
              name: Build Application
              image: rikorose/gcc-cmake
              size: 2x
              script:
                # Update the submodules
                - git submodule update --recursive --init
                # Install the dependencies
                - apt-get update && export DEBIAN_FRONTEND=noninteractive
                - apt-get -y install --no-install-recommends uuid-dev libssl-dev libz-dev libzmq5 libzmq3-dev
                # Print the Linux version.
                - uname -a
                # Print the gcc version.
                - gcc --version
    
                # Print the CMake version.
                - cmake --version
                # Setup the build
                - mkdir _bld && cd _bld
                # Call CMake
                - cmake -DCMAKE_BUILD_TYPE=Debug ..
                # Build project
                - make -j10
          - step: &Build-Container
              name: Test Container Build
              size: 2x
              script:
                # Update the submodules
                - git submodule update --recursive --init
                # Build the container
                - docker build --file ./Dockerfile .
              services:
                - dockerCode language: PHP (php)

    In this snippet, I define which image to use by default for all the steps. I chose to use the default Atlassian image, but specified version 3. If you do not specify a version here (with the :3) you wind up with a really old version of the image that is kept around for backwards compatibility.

    I also define two common build steps, called Build-Application and Build-Container, which I use later on in my pipeline.

    Define the pipeline(s)

    pipelines:
      custom:
        generate-release:
          - step:
              name: Generate release branch
              script:
                - git checkout master
                - git pull --ff-only
                - git checkout -b release/next
                - git push -u origin release/next
      pull-requests:
        '**': # all PRs
          - step: *Build-Application
          - step: *Build-Container
      branches:
        master:
          - step: *Build-Application
          - step: *Build-ContainerCode language: PHP (php)

    This snippet generates a few pipelines, one that runs each time the master branch is updated on the server, one that runs for every pull request created, and one custom pipeline that must be run manually.

    The master branch and pull-requests pipelines are identical and simply utilize the defined steps from the previous step. The custom pipeline however has a single role: create a new branch called release/next and push that back to the server. As you’ll see in the next section, this will trigger another branch pipeline.

    Define the release generation pipeline

      branches:
        # master branch defined here previously
        release/next:
          - step:
              name: Generate Release
              script:
                # Configure npm to work properly as root user in Ubuntu
                - npm config set user 0
                - npm config set unsafe-perm true
                # Install necessary release packages and generate release, pushing back to repo
                - npm install -g release-it @release-it/conventional-changelog @j-ulrich/release-it-regex-bumper --save-dev
                - release-it --ci
          - parallel:
            - step:
                name: Publish to External Continuous Delivery System
                script:
                  - export APP="name_of_app"
                  - git clone --recursive https://url.of.your.cd.com/your-cd-repo.git
                  - cd containers
                  - git checkout testing
                  - git submodule update --recursive --init
                  - cd ${APP} && git checkout master
                  - git pull
                  - export VERSION=$(git tag | sort -V | tail -1)
                  - >
                    echo "Updating ${APP} to Release Version: ${VERSION}"
                  - git checkout ${VERSION}
                  - cd ../
                  - git add ${APP}
                  - >
                    git -c user.name='Bitbucket Pipeline' -c user.email='bitbucket-pipeline@witricity.com' commit -m "${APP}: update to version ${VERSION}"
                  - git push
            - step:
                name: Create Pull Request
                caches:
                  - node
                script:
                  - apt-get update
                  - apt-get -y install curl jq
                  - export DESTINATION_BRANCH="master"
                  - export CLOSE_ME="true"
                  - >
                    export BB_TOKEN=$(curl -s -S -f -X POST -u "${BB_AUTH_STRING}" \
                      https://bitbucket.org/site/oauth2/access_token \
                      -d grant_type=client_credentials -d scopes="repository" | jq --raw-output '.access_token')
                  - >
                    export DEFAULT_REVIEWERS=$(curl https://api.bitbucket.org/2.0/repositories/${BITBUCKET_REPO_OWNER}/${BITBUCKET_REPO_SLUG}/default-reviewers \
                      -s -S -f -X GET \
                      -H "Authorization: Bearer ${BB_TOKEN}" | jq '.values' | jq 'map({uuid})' )
                  - >
                    curl https://api.bitbucket.org/2.0/repositories/${BITBUCKET_REPO_OWNER}/${BITBUCKET_REPO_SLUG}/pullrequests \
                      -s -S -f -X POST \
                      -H 'Content-Type: application/json' \
                      -H "Authorization: Bearer ${BB_TOKEN}" \
                      -d '{
                            "title": "Release '"${BITBUCKET_BRANCH}"'",
                            "description": "Automated PR release :)",
                            "source": {
                              "branch": {
                                "name": "'"${BITBUCKET_BRANCH}"'"
                              }
                            },
                            "destination": {
                              "branch": {
                                "name": "'"${DESTINATION_BRANCH}"'"
                              }
                            },
                            "close_source_branch": '"${CLOSE_ME}"',
                            "reviewers": '"${DEFAULT_REVIEWERS}"'
                          }'
    Code language: PHP (php)

    This is a rather large block, but is pretty straight-forward.

    The first step, called “Generate Release”, is where the release magic happens. It uses the NPM tool called release-it to generate the release. Basically, this utilizes a configuration file in the repository named .release-it.json. Based on that file, it will automatically do the following:

    • Bump the version, based on how you define it in .release-it.json
    • Generate and update a changelog
    • Git commit, tag, and push
    • And much more if you so choose…

    Since this is run in the release/next branch, the version, changelog and all other changes are made and pushed here. At that point, I wanted to do two things: first, publish this new release to my external continuous delivery system; and second, automatically generate a pull request in Bitbucket to get the release back in the master branch.

    Note, that when installing release-it on Ubuntu 22.04 (or older), you may run into issues with older versions of nodejs. To remedy this, run these commands:

    # Remove old version of nodejs
    sudo apt-get purge nodejs
    sudo apt-get autoremove # remove any lingering dependencies
    
    # Install updated nodejs (20 is latest at time of this writing)
    curl -sL https://deb.nodesource.com/setup_20.x | sudo -E bash -
    sudo apt-get install -y nodejs
    
    # Finally, install release-it
    npm install -g release-it @release-it/conventional-changelog @j-ulrich/release-it-regex-bumper --save-dev
    Code language: Bash (bash)

    My .release-it.json file looks like this:

    {
      "git": {
        "commitMessage": "[skip ci] ci: release v${version}"
      },
      "plugins": {
        "@release-it/conventional-changelog": {
            "preset": {
                "name": "conventionalcommits",
                "commitUrlFormat": "{{host}}/{{owner}}/{{repository}}/commits/{{hash}}",
                "compareUrlFormat": "{{host}}/{{owner}}/{{repository}}/compare/{{currentTag}}..{{previousTag}}",
                "types": [
                  {
                    "type": "feat",
                    "section": "Features"
                  },
                  {
                    "type": "fix",
                    "section": "Bug Fixes"
                  },
                  {
                    "type": "perf",
                    "section": "Performance Improvements"
                  }
                ]
            },
            "infile": "CHANGELOG.md"
        },
        "@j-ulrich/release-it-regex-bumper": {
            "out": [
                {
                    "file": "CMakeLists.txt",
                    "search": "VERSION {{semver}}",
                    "replace": "VERSION {{versionWithoutPrerelease}}"
                },
                {
                    "file": "Dockerfile",
                    "search": "Version={{semver}}",
                    "replace": "Version={{versionWithoutPrerelease}}"
                }
            ]
        }
      }
    }
    Code language: JSON / JSON with Comments (json)

    At this point, to generate a release, just go to the pipelines page for your repository and select Run pipeline. Then choose what branch you want to use for the basis of your release (I typically release from master) and choose the ‘custom: generate-release’ pipeline and off you go!

    Conclusion

    This process greatly simplifies my life when it comes to release a new version of my projects. Could this be fully automated? Absolutely — I’m just not there quite yet.

    I hope you find this useful!

    Links

  • 6 Tips for an Absolutely Perfect Little Code Review

    Code reviews are an important part of the software development process. They help ensure that code meets certain standards and best practices, and they can also help improve code quality by catching errors early on. However, code reviews can also be a source of frustration for developers if they’re not done correctly.

    Image Credit Manu Cornet @ Bonker’s World

    As a code reviewer, your job is to help make the code better. This means providing clear and concise feedback that helps the developer understand what works well and what needs improvement. A mindful, conscious approach to code reviews can yield incredible dividends down the road as you build not only a solid, reliable codebase; but strong relationships of trust with your fellow contributors.

    Here are some initial guidelines or best practices for performing a great code review:

    • Read the code thoroughly before commenting. This will help you get a better understanding of what the code is supposed to do and how it works.
    • Be specific in your comments. If there’s something you don’t like, explain why. Simply saying “this doesn’t look right” or “I don’t like this” isn’t helpful.
    • Offer suggestions for how to improve the code. If you have a suggestion for how something could be done differently, provide details about the suggestion and even some links and other material to back it up.
    • Be respectful. Remember that the code you’re reviewing is someone else’s work. Criticizing someone’s work can be difficult to hear, so try to be constructive with your feedback. Respectful, polite, positive feedback will go a long way in making code review a positive experience for everyone involved.
    • Thank the developer for their work. Code reviews can be tough, so make sure to thank the developer for their efforts.

    Following these practices will help ensure that code reviews are a positive experience for both you and the developer whose code you’re reviewing.

    In addition to these, here are a few specifics I look for when performing a code review:

    0. Does the code actually solve the problem at hand?

    Sometimes when I get into code reviews I forget to check if the written software meets the requirements set forth. As you do your initial read-through of the code, this should be your primary focus. If the code does not solve the problem properly or flat out misses the mark, the rest of the code review is pointless as much of it will likely be rewritten. There’s nothing worse than spending an hour reviewing code only to find out later that it doesn’t work. So save yourself the headache and make sure the code compiles and does what it’s supposed to do before you start.

    1. Is the code well written and easy to read? Does the code adhere to the company’s code style guide?

    It’s important to format code consistently so that code reviews are easier to perform. Utilizing standard tools to format code can help ensure that code is formatted consistently. Additionally, the code should be reviewed according to a standard set of guidelines. Many formatters will format the code per a chosen (or configured) style, handling all the white space for you. Other stylistic aspects to look for are naming conventions on variables and functions, the casing of names, and proper usage of standard types.

    Structural and organizational standards are important as well. For example, checking for the use of global variables, const-correctness, file name conventions, etc. are all things to look out for and address at the time of the code review.

    Last of the color coding” by juhansonin is licensed under CC BY 2.0.

    2. Is the code well organized?

    Well-organized code is very subjective, but it is still something to look at. Is the structure easy to follow? As a potential maintainer of this code, are you able to find declarations and definitions where you would expect them? Is the module part of one monolithic file or broken down into digestible pieces that are built together?

    In addition, be sure to look out for adequate commenting in the code. Comments should explain what the code does, why it does it, and how it works, especially to explain anything tricky or out of the ordinary. Be on the lookout for spelling errors as well because a well-commented codebase rife with spelling errors looks unprofessional.

    3. Is the code covered by tests? Are all edge cases covered? What about integration testing?

    Anytime a new module is submitted for review, one of the first things I look for are unit tests. Without a unit test, I almost always reject the merge/pull request because I know that at some point down the road a small, insignificant change will lead to a broken module that cascades through countless applications. A simple unit test that checks the basic functionality of the module, striving for 100% coverage, can save so much time and money in the long term. Edge cases are tricky, but if you think outside the box and ensure that the unit test checks even the “impossible” scenarios, you’ll be in good shape.

    Integration tests are a different matter. In my line of work, integration testing must be done with hardware-in-the-loop and that quickly becomes cost-prohibitive. However, as integration tests are developed and a test procedure is in place, any and all integration tests must be performed before a change will be accepted; especially if the integration test was modified in the change!

    4. Are there any code smells?

    Common code smells I look out for are:

    • Code bloat: long functions (> 100 lines), huge classes
    • Dispensable code: duplication (what I look for the most), stray comments, unused classes, dead code, etc.
    • Complexity: cyclomatic complexity greater than 7 for a function is prime fodder for refactoring
    • Large switch statements: could you refactor to polymorphic classes and let the type decide what to do?

    Many other smells exist – too many to check in detail with each code review. For a great deep-dive I refer you to the Refactoring Guru. Many static analysis tools will check for various code smells for you, so be sure to check the reports from your tools.

    The presence of a code smell does not mean that the code must be changed. In some cases, a refactor would lead to more complex or confusing code. However, checking for various smells and flagging them can lead to discussions with the developer and produce a much better product in the end!

    5. Would you be happy to maintain this code yourself?

    One of the last things I check for during a code review is whether I would be okay to maintain this code on my own in the future. If the answer is no, then that turns into specific feedback to the developer on why that is the case. Maybe the structure is unclear, the general approach is questionable, or there are so many smells that it makes me nervous. Typically in cases like this, I find it best to give the developer a call (or talk with them in person) and discuss my misgivings. An open, honest discussion about the potential issues often leads to added clarity for me (and it’s no longer an issue) or added clarity for them and they can go fix the problem.

    These are just a few of the specific things I look for, but following the practices above will help make code reviews a positive experience for everyone involved. What are some best practices or tips you have found to lead to a good code review? Thanks for reading!