profile picture

bashunit

Turning frustrations into tools for better development

October 30, 2024 - 1426 words - 8 mins Found a typo? Edit me
software testing open-source

cover

bashunit is a lightweight, easy-to-use testing framework for Bash, packed with handy features like parallel and snapshot testing, test doubles, data providers, and tons of built-in assertions. Backed by clear docs and an active community, it’s become a favorite for reliable Bash testing. What started as a simple dev frustration has grown into an open-source tool that makes testing in Bash a lot easier and fun.

  1. The story behind bashunit
  2. Why create another testing library?
  3. How is it nowadays?
  4. Core features
  5. Lightning tech talk

The story behind bashunit

The journey to create bashunit started from a simple frustration: I worked with a team where each commit had to start with the ticket name. Since I like working in small steps with quick, iterative commits, adding the ticket key and number to every single commit became a major hurdle, slowing down my development flow with unnecessary friction.

After a few days of this, I decided to automate it. Git has a helpful hook, prepare-commit-msg, which allows you to alter commit messages before they’re finalized. I created a Bash (script) that automatically fetches the ticket key and number from the branch name and inserts it into the commit message, making my process smoother and more efficient.

As someone who values continuous improvement, I began adding more features to this script. However, it became clear that maintaining and testing these changes manually was taking too long and was prone to error. To make development safer and more efficient, I created an assert(link) function, enabling automated tests that verified the expected behavior based on script output.

bashunit-original-assert.jpg

The assert function allowed me to define multiple assertions in a separate file, making it easy to validate that any refactoring of the original hook maintained the expected behavior. If a change inadvertently broke existing functionality, it would instantly flag the issue, letting me know right away that something needed fixing. This setup provided immediate feedback and helped ensure that any updates to the script didn’t disrupt its intended logic. For example:

conventional-commits-original-tests.jpg

In the example above, you’ll notice that I execute the actual “SCRIPT” as the second argument in the assert function, comparing its output to the expected value provided as the first argument. Here, we have two test cases, each exporting TEST_BRANCH to simulate how the commit message would vary based on the branch name. This setup emulates real behavior, allowing us to test how different branch names affect the commit message formatting. More examples here.

I decided to separate the assert function from the test cases, as shown here, to keep things modular and reusable. Then, I created a runner to execute each test case independently, reducing test interference and improving reliability. You can see that setup here. This structure made automated testing easier and refactoring safer.

conventional-commits-call_test_functions.jpg

This was a great improvement because it made it easier to separate test cases from the logic of the test runner itself. This clear structure has simplified both creating and managing tests.

conventional-commits-refactor-test-cases.jpg

Now the tests were organized and I got an idea:

cover

## Follow-up idea
Separate the testing logic into another repo, 
so it could be reused anywhere.

And so it began. At the time, I didn’t know much about Bash or the best ways to use a Bash project as a dependency. But I knew I could start by using a Git submodule, even though I’m not a huge fan of them.

On September 4, 2023, I launched version 0.1, which featured a working runner and a single assertion function: assertEquals. Later, version 0.2 was released, allowing ./bashunit to be a standalone executable, runnable from any folder. Here’s how it looked back then:

bashunit-02-demo.jpg

I shared the project with a few friends who quickly jumped in to help with documentation, the website, additional assertions, snapshot testing, and key decisions. To emphasize its open-source spirit and community ownership, I moved it to an organization we created specifically for sharing OSS projects –making it a truly collaborative project rather than an individual one.

Why create another testing library?

I know now that other Bash testing libraries exist. But when I started with bashunit, I wasn’t aware of them, and frankly, I’m still no Bash expert. By the time I learned about these alternatives, it was already too late—bashunit had gained enough momentum and excitement to keep going strong.

While those other libraries may serve specific use cases, use modern Bash, or be developed by more seasoned Bash developers, bashunit aims to set itself apart by offering a great developer experience, shaped by years of working with various testing frameworks.

I was asked about the differences on September 7, 2023, and here’s my response: Question: Difference to pgrange/bash_unit.

How is it nowadays?

Today, you can install bashunit via curl, Homebrew, MacPorts, by downloading the latest GitHub release from GitHub, or even by building it yourself from source. Completely open-source.

The project is written in Bash 3.2 (from 2007) since that’s the default version on macOS –even now, in 2024. This compatibility means bashunit runs smoothly on that version, and I plan to maintain support for it.

To ensure quality, we test each feature with unit, functional, and acceptance tests, making bashunit its own “first user” of every new feature. We also have several CI workflows using GitHub actions that run tests on different platforms to check compatibility and confirm everything works as promised.

bashunit-ci.jpg

In June 2024, bashunit was integrated in PHPStan for their end-to-end tests, enabling the use of bashunit’s assertions independently of its runner. This flexibility turned out to be very useful.

Last summer, I was invited to speak about bashunit at the International PHP Conference in Berlin, alongside Manu, another contributor. This project has opened doors and led to a lot of gratitude from users who appreciate the work we’ve put into it.

Core features

bashunit includes classic lifecycle functions like set_up, tear_down, set_up_before_script, and tear_down_after_script.

It also supports a wide range of command-line parameters and configuration values. Some of my favorites include:

We also provide data providers for running the same test cases with different inputs.

For test doubles, bashunit offers mocks and spies. These work within the same process as the test, but currently, they don’t work across processes—an area for improvement.

Powerful snapshot testing is included, making it easy to verify command or script outputs over time.

bashunit offers a large set of native assertions for test cases, including:

You can even create your own custom asserts to extend bashunit’s capabilities.

With over 25 contributors and more than 325 stars on GitHub within just a year of spare-time development, I’m genuinely proud of what this project has become.

Lightning tech talk

Recently, I presented a lightning tech talk at a hackers’ meetup, demoing bashunit to an audience of over 100 people. It was an incredible experience to share this tool with such an engaged crowd!


bashunit original logo designed by Antonio.