DEV Community

Cover image for A Better Way To Code: Documentation Driven Development
Corbin Crutchley for This is Learning

Posted on • Originally published at unicorn-utterances.com

A Better Way To Code: Documentation Driven Development

If you've spent much time in software development, you've undoubtedly heard the expression “test-driven development” or "TDD" for short.

The idea behind TDD is that you should write tests before programming an implementation. For example, say you want to implement a function called calculateUserScore based on a user's K/D in a video game. According to TDD, you should start by writing unit or integration tests to validate the input to an expected set of outputs.

Starting with tests can be a great help to ensure that your program runs the way it's intended when all is said and done. One downside, however, is that tests are still a form of coding; Yes, even when you follow good testing practices by hardcoding values and avoiding complex logic. It's still software development, and your tests still need to pass at the end of the day.

Making sure tests pass can be challenging to handle with the unknowns of implementation detail. After all, if you expect parseInt to act one way and it behaves another, you will likely have to rewrite all tests that worked off that assumption.

As a result, many choose to start implementing a function as a proof-of-concept, then adding tests incrementally alongside implementation: A TDD-lite, so to speak.

The problem is that, by doing so, you lose one of the most significant benefits of test-driven development: Its ability to force you to confront your API ahead of time.

APIs are hard

You're working at an indie game company. A small top-down shooter that you've written in JavaScript with Phaser. Your bass has asked you to implement a user score.

"No problem, calculateUserScore is going to be super simple - no need to overthink it."

You think, typing out a basic implementation:

function calculateUserScore({kills, deaths}) {
    return parseInt(kills / deaths, 10)
}
Enter fullscreen mode Exit fullscreen mode

But wait! What about assists? Do those count? Surely they should. Let's treat them as half of a kill.

function calculateUserScore({kills, deaths, assists}) {
    const totalKills = kills + (assists / 2);
    return parseInt(totalKills / deaths, 10)
}
Enter fullscreen mode Exit fullscreen mode

Oh, but some kills should give bonus points. After all, who doesn't love a good 360 no-scope? While kills was simply a number before, let's change it to an array of objects like so:

const killsArr = [
     {
          additionalPoints: 3
     }
]
Enter fullscreen mode Exit fullscreen mode

Now we can change out the function implementation for this:

function calculateUserScore({killsArr, deaths, assists}) {
    const kills = killsArr.length;
    const additionalPoints = killsArr.reduce((prev, k) => k.additionalPoints, 0);
    const totalKills = kills + (assists / 2);
    return parseInt((totalKills / deaths) + additionalPoints, 10);
}
Enter fullscreen mode Exit fullscreen mode

While we've seen the function change, remember that your game may be making this calculation in multiple parts of the codebase. On top of this, maybe your API still isn't perfect for this function. What if you want to display the special kills with additional points after a match?

These drastic refactors mean that each iteration requires additional refactor work, likely delaying the time to ticket completion. This can impact releases dates or other scheduled launches.

Let's take a step back. Why did this happen?

These problems tend to happen because of miscommunication of scope. This miscommunication can be introduced between teams, from individual to individual, or even simply within your internal monolog.

Testing is hard

One way that many suggest working around this problem is by following TDD. TDD can help force you to address your API ahead of time by adding in a feedback loop.

For example, before implementing the calculateUserScore function into your codebase, you might test against the first implementation, add a test.todo to add in assists, and realize you should update your API before moving forward.

However, while TDD forces you to address your API, it doesn't help you distinguish scope. This limitation of understanding of your scope may then, in turn, impact your API.

Let me explain:

Let's say that the ability to track special kills after the fact isn't possible to display on the match end until later in the development cycle. You know this and have decided to stop at the second implementation where kills is still a number. However, because the function is used repeatedly in the codebase, you'll need to do a larger refactor at a later date.

Had you spoken with other engineers, you may have realized that developments in the match-end screen were completed sooner than expected. Unfortunately, it's only caught now in code review after you've made the implementation, forcing a refactor immediately.

Getting to the point

Okay, okay, I'll get to the point: There's a better way to address this "API shift" problem better than TDD. This "better way" is "Documentation driven development."

Drake looking away from "Test Driven Development" but a thumbs up for "Documentation Driven Development"

Writing docs first can help you iron out implementation details ahead of time before making tough calls about implementing a design. Even reference APIs can help you make a lot of designs.

Let's loop back to the older example of calculateUserScore. Just like before, you called a short meeting to gather the requirements from the team. This time though, you start by writing documentation instead of starting with the code.

You include a mention of what the API should look like based on these requirements:

/**
 * This function should calculate the user score based on the K/D of the
 * player.
 *
 * Assists should count as half of a kill
 *
 * TODO: Add specialty kills with bonus points
 */
function calculateUserScore(props: {kills: number, deaths: number, assists: number}): number;
Enter fullscreen mode Exit fullscreen mode

You also decide to showcase some usages in your docs:

caluculateUserScore({kills: 12, deaths: 9, assists: 3});
Enter fullscreen mode Exit fullscreen mode

While working through these docs, you decide to quickly sketch out what the future API might look like when bonus points are added.

/*
 * TODO: In the future, it might look something like this to accommodate
 * bonus points
 */
calculateUserScore({kills: [{killedUser: 'user1', bonusPoints: 1}], deaths: 0, assists: 0});
Enter fullscreen mode Exit fullscreen mode

After writing this, you realize you should utilize an array for the kills property first rather than later on. You don't have to have bonus points, but instead, you can simply track an unknown user for each kill and change it in the future.

calculateUserScore({kills: [{killedUser: 'unknown'}], deaths: 0, assists: 0});
Enter fullscreen mode Exit fullscreen mode

While this might seem obvious to us now, it may not be so clear at the moment. This is the benefit of Documentation-Driven Development: It forces you to go through a self-feedback cycle on your APIs and the scope of your work.

Refining the process

OK, I get it. Documentation is seen as a chore. While I could go on about "your medicine is good for you," I've got good news for you: Documentation doesn't mean what you think it means.

Documentation can be found in many forms: design mockups, API reference docs, well-formed tickets, future plan writeups, and more.

Essentially, anything that can be used to communicate your thoughts on a topic is documentation.

In fact, this includes tests. 😱 Tests are a good way of conveying API examples for your usage. TDD itself may be enough on its own to convey that information for future you, while other times, it may be a good companion alongside other forms of documentation.

In particular, if you're good about writing primarily integration tests, you're actually writing out usage API docs while writing testing code.

This is particularly true when writing developer tooling or libraries. Seeing a usage example of how to do something is extremely helpful, especially with a test to validate its behavior alongside it.


Another thing "documentation-driven development" does not prescribe is "write once and done." This idea is a myth and may be harmful to your scope and budgets - time or otherwise.

As we showed with the calculateUserScore example, you may need to modify your designs before moving forward for the final release: that's okay. Docs influence code influence docs. The same is true for TDD.


DDD isn't just useful for developing code for production, either. In interviews, some good advice to communicate your development workflow is to write code comments and then write the solution. This allows you to make mistakes in the documentation phase (of writing comments) that will be less time-costly than if you'd made a mistake in implementation.

By doing this, you can communicate with your interviewer that you know how to work in a team and find well-defined goals. These will allow you to work towards an edgecase-free* implementation with those understandings.

Bring it back now y'all

I realize this article already has more twists than an M. Night Shyamalan film, but here’s one more; documentation driven development, as we’ve explored today, is an established concept. It’s simply called by other names:

Each of these refers to a form of validating the functionality of code behind user behavior. Each encourages a stronger communication method that often includes documentation in the process. "DDD" is just another form of this type of logic.

Conclusion

I've been using documentation-driven development as a concept to drive my coding on some projects. Among them was my project CLI Testing Library, which allowed me to write a myriad of documentation pages as well as verbose GitHub issues.

Both of these forced me to better refine my goals and what I was looking for. The end-product, I believe, is better as a result.

What do you think? Is "DDD" a good idea? Will you be using it for your next project?

Let us know what you think, and join our Discord to talk to us more about it!

Discussion (17)

Collapse
jhelberg profile image
Joost Helberg

Can i upvote this to make it a headline in every dev publication? Seriously impactful! I use literate programming myself, writing docs first and code along the way. Most people do before they think, well, do docs first then, the thinking goes in parallel. Better code will follow.

Collapse
crutchcorn profile image
Corbin Crutchley Author

This is one of the kindest comments I've received on anything I've written - thank you.

Collapse
lob profile image
Lob

Nice!! Just stumbled on GitHub star Monica Powell’s four-part series focusing on Document-Driven GROWTH, which is a nice complement. (This is part 4 but it’s the only one that links to all the others:) github.com/readme/guides/document-...

Collapse
crutchcorn profile image
Corbin Crutchley Author

I will for sure have to check it out, Monica does great work!

Collapse
natescode profile image
Nathan Hedglin • Edited on

Swagger UI UI helps automatically document APIs. Good comments are always helpful too.

Collapse
crutchcorn profile image
Corbin Crutchley Author

Swagger can be a great tool to experiment with API design, especially when tied into hard coded data for a real API feel that you can incrementally build around and get feedback for!

Collapse
mmuller88 profile image
Martin Muller • Edited on

Very cool article. DDD seems taken by Domain Driven Design. And perhaps that goes into a similar direction as this article.

Collapse
crutchcorn profile image
Corbin Crutchley Author

I was actually already familiar with Domain Driven Design, but I couldn't think of an alternative term than "Documentation Driven Design" to clearly and quickly articulate my thoughts

Collapse
rajeshroyal profile image
Rajesh Royal

you can take advantage of tsdoc or jsdoc.

Collapse
crutchcorn profile image
Corbin Crutchley Author

You absolutely can, to be fair. Just another tool in the toolbox of documentation

Collapse
westial profile image
Jaume Mila Bea • Edited on

I'm sorry, i didn't agree at all. TDD is documentation and testing together. If you forget to update the test when a function changed, the tests will fail. Deprecated documentation does not fail. Once you have deprecated documentation you don't rely on it because you don't know which one is updated and which one didn't.
About the reading complexity of the test suites, you can try with BDD and gherkins.
A method in programming has to explain what it does within the function name.
You like to read, then after your mentions about TDD I think that you really don't know TDD. I recommend you reading first the: Test-Driven Development by Example (Kent Beck), from the mastermind of it.

Collapse
alvesvalentin profile image
Alvès Valentin

Thank you !
Comments in code are just noise and they're practically never up to date. Unit tests are there to specifiy a behavior (business rules) you can easily use the tests as documentation. The truth (what the product actually does) is in the code not in the documentation, tests are there to be sure behavior didn't changed and act as documentation, i never trust documentation i only trust what i see in the code

Collapse
kerryconvery profile image
kerryconvery • Edited on

Yes, completely agree. Documentation that is not automatically validated against the code for correctness is the worst kind imho. This is because over time it becomes outdated and paints a different picture of what the system is doing and how; and it will get outdated.

However imho the code + the tests, together, form a self validating single source of truth of documentation. The tests document what the system does and the code documents how it does it.
And they both keep each other honest.

Collapse
seibernaut profile image
seibernaut

Talk about reading, let's read too "Why are we yelling? : the art of productive disagreement" by Buster Benson.

Collapse
jayjeckel profile image
Jay Jeckel

Great article! As I say to TDD cultists when they advocate their propaganda, if you're using TDD to design your api, then you're starting on the wrong foot. The design should be worked out before any production code or tests are written, though I prefer the term Design Driven Development.

Collapse
crutchcorn profile image
Corbin Crutchley Author

See, I'm not even convinced TDD is entirely on the wrong foot, especially when applied with nuance. Clean tests can serve an important role in providing documentation for behavior in code.

For me, it's when tests are seen as the ONLY form of planning or documentation for a codebase, or even consume a majority of time to take away from other forms of documentation that I find a problem.

Similarly types aren't a perfect form of docs, but can be of help. It's all about balancing what information you're providing new devs in different mediums to create a whole story

Collapse
peerreynders profile image
peerreynders

Hammock Driven Development with minutes.