--- title: >- Why I like the Contract-Based Dependency Management idea date: 2021-10-13 02:05 modified: 2021-10-13 02:05 lang: en authors: rysiek tags: - programming - tests status: published pinned: false --- About a week ago, [`@tomasino`](https://tilde.zone/@tomasino) published [a post on his *contract-based dependency management*](https://labs.tomasino.org/contract-based-dependency-management/) idea (aka `CBDM`), and I would be lying if I said I didn't like it. Not only does it provide a better model for dependency management than [SemVer](https://semver.org/) or any other versioning scheme, but it *also*: - provides strong incentive for developers to maintain extensive test suites for their own software; - provides strong incentive for developers to help developers of their project's dependencies maintain extensive test suites, too; - provides very clear and unambiguous information on whether or not some functionality or behaviour of the dependency is, in fact, officially supported by dependency's developers, or not; - provides very clear and unambiguous information if some functionality or behaviour of the dependency has changed; - makes it very, very clear who done goofed if a dependency upgrade breaks a dependent project. ## What's CBDM? The basic idea boils down to this: when deciding if a given version of a given dependency is compatible with a dependent piece of software, instead of relying on *version numbers* -- rely on *tests* that actually verify the functionality and behaviour that piece of software actually depends on. In other words, when considering updating dependencies of a project, don't look at version numbers, but look at tests of the dependency (and their results). Tomasino's post goes into more detail and is well-worth a read. ## What's wrong with version numbers? Version numbers are are notoriously unreliable in predicting if something breaks after the upgrade. That's the whole point of SemVer -- to try to make them more reliable. The problem is that it's impossible to express, in a set of just few numbers, all the dimensions in which a piece of software might change. More importantly, certain changes might be considered irrelevant or minor by the developers, but might break projects that depend on some specific peculiarity. Cue specifications, and endless debates whether or not a particular change breaks the specification or not. ## How could CBDM work in practice? Let's say I'm developing a piece of software, call it `AProject`. It depends on a library, say: `LibBee`. `LibBee` developers are Gentlefolk Scholars, and therefore `LibBee` has quite extensive test coverage. As the developer of `AProject` I specify the dependency not as: > `LibBee` ver `x.y.z` ...but as: > `LibBee`, *(list of upstream tests I need to be unchanged, and to pass)* (Bear with me here and let's, for the moment, wave away the question of how exactly this list of upstream tests is specified.) This list does not need to contain *all* of `LibBee`'s tests -- in fact, it should *not* contain all of them as that would effectively pin the current exact version of `LibBee` (assuming full coverage; we'll get back to that). However, they *should* be tests that test *all* of `LibBee`'s functionality and behaviour `AProject` does rely on. This set of tests becomes a *contract*. As long as this contract is fulfilled by any newer (or older) version of `LibBee` I know it should be safe for it to be upgraded without breaking `AProject`. ### What if a `LibBee` upgrade breaks `AProject` anyway? I say "should", because people make mistakes. If upgrading `LibBee` breaks `AProject` even though the contract is fulfilled (that is, all specified tests have not been modified, and are passing), there is basically only a single option: `AProject` relied on some functionality or behaviour that was *not in the contract*. That makes it very clear who is responsible for that unexpected breakage: I am. I failed to make sure the contract contained everything I needed. Thus a long and frustrating blame-game between myself and `LibBee`'s developers is avoided. I add the information about the additional test to the contract, and deal with the breakage as in any other case of dependency breaking change. `AProject` just got a better, more thorough dependency contract, and I didn't waste any time (mine nor `LibBee` developers') blaming anyone for my own omission. Win-win! ### What if the needed upstream test does not exist? If a test does not exist upstream for a particular functionality or behaviour of `LibBee` that I rely on, it makes all the sense in the world for me to write it, and submit it as a merge request to `LibBee`. When that merge request gets accepted by `LibBee`'s developers, it clearly means that functionality or behaviour is supported (and now also tested) upstream. I can now add that test to `AProject`'s dependency contract. `LibBee` just got an additional test contributed and has more extensive test coverage, for free. My project has a more complete contract and I can be less anxious about dependency upgrades. Win-win! ### What if the needed test is rejected? If `LibBee` developers reject my merge request, that is a very clear message that `AProject` relies on some functionality or behaviour that is *not officially supported*. I can either decide to roll with it, still add that test to the contract, and keep the test itself in `AProject` to check each new version of `LibBee` when upgrading; or I can decide that this is too risky, and re-write `AProject` to not rely on that unsupported functionality or behaviour. Either way, I know what I am getting into, and `LibBee`'s developers know I won't be blaming *them* if they change that particular aspect of the library -- after all, I've been warned, and have a test to prove it. You guessed it: win-win! ### Abolish version numbers, then? No, not at all. They're still useful, even if just to know that a dependency has been upgraded. In fact, they probably should be used alongside a test-based dependency contract, allowing for a smooth transition from version-based dependency management to CBDM. Version numbers work fine on a human level, and with SemVer they carry some reasonably well-defined information. They are just not expressive enough to rely on them for dependency management. Anyone who has ever maintained a large project with a lot of dependencies will agree. ## Where's the catch? There's always one, right? The difficult part, I think, is figuring out three things: 1. How does one "*identify a test*"? 2. What does it mean that "*a test has not changed*"? 3. How to "*specify a test*" in a dependency contract? The answers to 1. and 2. will almost certainly depend on the programming language (and perhaps the testing framework used), and will almost certainly mostly define the answer to 3. One rough idea would be: 1. A test is identified by it's name (basically every unit testing framework provides a way to "name" tests, often *requiring* them to be named). 2. If the code of the test changes in any way, the test is deemed to have changed. Probably makes sense to consider some linting first, so that whitespace changes don't invalidate the contracts of all dependent projects. 3. If a test is identified by it's name, using that name is the sanest. I really think the idea has a lot of merit. Software development is becoming more and more test-driven (which is great!), why not use that to solve dependency hell too?