DEV Community

Dennis
Dennis

Posted on

Learning TDD by doing: Business hours

Remember: 🔴 red, 🟢 green, ♻️ refactor

I'm maintaining a website with a feature to display the business hours for the next 7 days. I received a new feature request, because the business hours as implemented didn't really work. I used Test Driven Development to build it.

Context

The feature as implemented worked as follows: The content editor in Umbraco has regular opening hours for every day of the week. So they may for example configure that the business is open on every Monday from 8:00 to 16:00 and every Tuesday from 9:00 to 17:00 for example. For special dates, like christmas, they have a list of special dates when opening hours are different from usual. They may be closed on a specific day or may be open longer or shorter.

This approach became problematic, because oftentimes the business hours were not regular every week. They may repeat every odd or even week or even just once per month. If the business wasn't open at least once in the next 7 days, there weren't any business hours to display.

In user story form, I would say: As a user, I want to know when the business is open, so that I know when I can visit. What it comes down to is that I must find the earliest date at which the business opens.

Implementing using TDD

Since the data in the backoffice was somewhat inconvenient to use, I decided to disregard it for a moment and start with an object with data in a form that I could easily work with.

The basic use cases

I thought my simplest use case was this: The Business hours calculator should not find a date if the business is never open. In code, this test looked like this:

[Fact]
public void ShouldBeClosedWhenNeverOpen()
{
    // given
    var sut = new OpeningHoursService([]);

    // when
    var result = sut.GetFirstOpen(new DateTime(2024, 9, 2, 12, 30, 0));

    // then
    Assert.Null(result);
}
Enter fullscreen mode Exit fullscreen mode

In hindsight, this was not a good place to start, because the easiest way to implement a function is to just return null. Now I had to go out of my way to return a dummy object, just so I could watch the test fail.

💡 Learning
test cases that expect default values as result are not good to start with when doing test driven development. It means you have to go out of your way to return a non-default value first to see the test fail and then delete it again to return the expected default value.

After that, it turned out to be quite easy to expand the design to fit all expected usecases:
"The business hours calculator should..."

  1. ... find today if the business hasn't closed yet
  2. ... not find today if the business has already closed
  3. ... find date one day ahead
  4. ... find date many days ahead

After that, I had all the basic cases covered. Here is one of the tests, so you can see how I wrote it:

[Fact]
public void ShouldBeOneDayAheadWhenClosedToday()
{
    // given
    var sut = new OpeningHoursService(_standardTimes);
    var currentDateTime = new DateTime(2024, 9, 2, 18, 0, 0);

    // when
    var result = sut.GetFirstOpen(currentDateTime);

    // then
    Assert.NotNull(result);
    Assert.Equal(new DateOnly(2024, 9, 3), result.Date);
}
Enter fullscreen mode Exit fullscreen mode

Looking back at this test, I think it can still be improved in some ways. I'm talking some more about this further down this article.

The more advanced use cases

Even the more advanced usecases turned out to be pretty easy to implement. I went through these steps:

"The business hours calculator should..."

  1. ... not find today if today is closed by special business hours
  2. ... override standard open and close times with special times
  3. ... find closest special business hours if not open within one week
  4. ... not find a date when it is closed by special business hours

Once again, looking back at this, I'm not sure if these were all the right steps, but at least it got me to a working implementation that was much more simple that what I first had in mind.

✅ Success!
I couldn't make up a whole solution in my mind at first, but using TDD, I was able to separate the problem into small steps. Instead of solving the whole problem at once, I started with a simple use case and slowly added more use cases and evolved my design accordingly. After every step, I could verify that all tests were still passing.

My thoughts in hindsight

This was one of the cases where I feel like TDD really helped me. Instead of just going through the motions for practice, it actually helped me to think about the problem better and it helped me get the desired results more quickly. What's even better: No bug reports!

That being said, I do still see a lot of potential to improve. Looking at the example test that I shared, it may be hard to understand why the Assert statement at the end is what it is, because the value that it asserts for is not visible in the 'given' part of the test. If I were to write this again, the given part would likely read more like this: "given now is 18:00, today is open until 17:00 and tomorrow is open"

💡 Learning
Tests are easier to read if you can already guess what will be tested for by reading the name of the test and the 'given' part. More technically: the 'given' part should put emphasis on the data that is relevant to the test, but hide the data that is not.

Also the more special cases seem somewhat technical still and don't really follow the expected format. As a user, I really don't care if a day is special or not. I just want to know when the business is open. So if I were to do this again, I think I should be more aware about user value instead of technical function. I'm not sure at this point though how I could do that.

❓ Uncertainty
I'm unsure how I could've written these tests in a way that doesn't reveal the distinction between regular hours and special hours.

Let me know in a comment what you think of my approach for this feature! Do you agree with the learnings that I wrote down?
Thank you for reading 😊

Top comments (0)