DEV Community

Cover image for How To Never Fail At The Fizzbuzz Interview Challenge
Alex Fedorov
Alex Fedorov

Posted on

How To Never Fail At The Fizzbuzz Interview Challenge

Ah, the classic FizzBuzz coding challenge.

I’ve decided to include this picture to get your attention. Oh, and also because it associates with the “Buzz” part of the challenge.

“Buzz” – alarm clock…

Onward! Do you know that you don’t have even a single excuse to fail at one of these? So you think that you know that coding kata well?

Let your imagination run wild:

You are at the coding interview, and the interviewer has just asked you to implement FizzBuzz and given you the following problem description:

Write a program that prints a sequence of numbers from 1 to N.

Every number that divides by the three should appear as “Fizz.”

Every number that divides by the five should appear as “Buzz.”

Every number that divides by the three and the five should appear as “FizzBuzz.”

– anonymous interviewer

A piece of cake!

So you are writing away, and the code is pouring out of you…

Five or ten minutes in and you are done. You feel the interviewer might just suspect that you were ready for the challenge. In that case, they will have some additional unexpected requirements for you.

Just like in the real world programming.

Continue reading, and we will explore these additional requirements and possible solution for them. And for now, we will solve the FizzBuzz challenge in Kotlin.

It is probably a good idea to have some automated tests so that we know when we are done. Let’s create a new test suite in JUnit4:

import org.junit.Assert.*
import org.junit.Test

@Suppress("FunctionName")
class FizzBuzzTest {

}
Enter fullscreen mode Exit fullscreen mode

Now we are going to write a first test – the simplest one, that covers the fact that we can produce a normal number sequence without any “fizzes” or “buzzes”:

@Test
fun `simple numbers`() {
    // ARRANGE
    val fizzBuzz = FizzBuzz()

    // ACT
    val sequence = fizzBuzz.generateSequence(size = 2)

    // ASSERT
    assertEquals(listOf("1", "2"), sequence)
}
Enter fullscreen mode Exit fullscreen mode

I like to split my tests in three sections: arrange, act and assert. It is not necessary to write out loud these comments as I do here. I don’t usually do myself so, but I think about these three sections when constructing a good test.

On the other hand, I highly recommend you DO split your tests in such three sections. And write down these comments too for the sake of explicitness.

If you are already comfortable writing great tests, feel free to ignore my advice.

Onward! There are few compile errors in this test that we will need to deal with. First, we haven’t defined FizzBuzz class yet. Second, we will need to create the generateSequence method.

Just to make things compile, we will make that method return an empty list:

class FizzBuzz {

    fun generateSequence(size: Int): List<String> {
        return emptyList()
    }

}
Enter fullscreen mode Exit fullscreen mode

Now we can run our test. As expected, it fails because we are expecting a list with two items ["1", "2"], but we are getting an empty list [].

Making this pass is pretty simple. We are going to generate a sequence from one to size using Kotlin’s range syntax:

1..size
Enter fullscreen mode Exit fullscreen mode

Then we are going to generate a new sequence out of it by calling toString() method on every item of that sequence. To do that we are going to employ map function defined on any Kotlin sequence:

fun generateSequence(size: Int): List<String> {
    return (1..size).map { it.toString() }
}
Enter fullscreen mode Exit fullscreen mode

Oh, by the way.

Wondering what all these neat features of Kotlin are? Maybe you want to learn how to build a full-fledged application in Kotlin?

I have accidentally written this 80-page hands-on getting started tutorial for Kotlin. Feel welcome to download the free PDF Ultimate Tutorial: Getting Started With Kotlin. You are going to learn how to build a full-fledged command-line application in Kotlin. You’ll get free access preview for Web app and Android app tutorials, as well!

Anyways, we should run our tests now.

And they all pass.

Alright. Our next step is to start handling some “fizz” numbers. For that, we can write a new test. It is as simple as copy-pasting (oh no!) the previous test and making some modifications to it:

@Test
fun `fizz numbers`() {
    // ARRANGE
    val fizzBuzz = FizzBuzz()

    // ACT
    val sequence = fizzBuzz.generateSequence(size = 4)

    // ASSERT
    assertEquals(listOf("1", "2", "Fizz", "4"), sequence)
}
Enter fullscreen mode Exit fullscreen mode

Now we are going from one to four, and we are expecting all numbers to be normal except for number three. It should become “Fizz” in the generated sequence.

If we run this test, it is going to fail, because so far we are producing only simple numbers converted to strings.

To make this test pass we are going to extract it.toString() part into a private method generateMethod(number: Int): String. We will add an “if” statement to handle the case when the number divides by three:

fun generateSequence(size: Int): List<String> {
    return (1..size).map { generateNumber(it) }
}

private fun generateNumber(number: Int): String {
    if (number % 3 == 0) {
        return "Fizz"
    }

    return number.toString()
}
Enter fullscreen mode Exit fullscreen mode

It passes the test suite now.

Next, we would like to write a test for the “Buzz” requirement. For that, we are going to generate a sequence from one to fourteen. There will be a few “fizzes” and “buzzes” there.

No “fizzbuzzes” yet, though.

@Test
fun `buzz numbers`() {
    // ARRANGE
    val fizzBuzz = FizzBuzz()

    // ACT
    val sequence = fizzBuzz.generateSequence(size = 14)

    // ASSERT
    assertEquals(listOf(
            "1", "2", "Fizz", "4", "Buzz",
            "Fizz", "7", "8", "Fizz", "Buzz",
            "11", "Fizz", "13", "14"), sequence)
}
Enter fullscreen mode Exit fullscreen mode

That test fails because we are not producing any “buzzes” yet. We will add another “if” statement to handle that case:

private fun generateNumber(number: Int): String {
    if (number % 3 == 0) {
        return "Fizz"
    }

    if (number % 5 == 0) {
        return "Buzz"
    }

    return number.toString()
}
Enter fullscreen mode Exit fullscreen mode

And the test is passing.

Finally, we want to write a test involving some “fizzbuzzy” numbers. In this case, we want to generate a sequence from one to thirty-one, so that we include at least two of these:

@Test
fun `fizz buzz numbers`() {
    // ARRANGE
    val fizzBuzz = FizzBuzz()

    // ACT
    val sequence = fizzBuzz.generateSequence(size = 31)

    // ASSERT
    assertEquals(listOf(
            "1", "2", "Fizz", "4", "Buzz",
            "Fizz", "7", "8", "Fizz", "Buzz",
            "11", "Fizz", "13", "14", "FizzBuzz",
            "16", "17", "Fizz", "19", "Buzz",
            "Fizz", "22", "23", "Fizz", "Buzz",
            "26", "Fizz", "28", "29", "FizzBuzz",
            "31"), sequence)
}
Enter fullscreen mode Exit fullscreen mode

And of course it does not work.

That is because the first “if” statement (for “fizzes”) catches this case and produces “Fizz.” To fix that we will have to write a new “if” statement that handles the case for “fizzes” and “buzzes” at the same time.

That case will have to be checked before all others. That way, nor “Fizz,” nor “Buzz,” will match before it:

private fun generateNumber(number: Int): String {
    if (number % 3 == 0 && number % 5 == 0) {
        return "FizzBuzz"
    }

    if (number % 3 == 0) {
        return "Fizz"
    }

    if (number % 5 == 0) {
        return "Buzz"
    }

    return number.toString()
}
Enter fullscreen mode Exit fullscreen mode

Alrighty! All tests are passing now.

Let’s extract some duplication in the test suite. Notably, the ARRANGE part. That is because it is the same for every single test. We will promote fizzBuzz local variable to the private field of the test suite class:

import org.junit.Assert.*
import org.junit.Test

@Suppress("FunctionName")
class FizzBuzzTest {

    private val fizzBuzz = FizzBuzz()

    @Test
    fun `simple numbers`() {
        // ACT
        val sequence = fizzBuzz.generateSequence(size = 2)

        // ASSERT
        assertEquals(listOf("1", "2"), sequence)
    }

    @Test
    fun `fizz numbers`() {
        // ACT
        val sequence = fizzBuzz.generateSequence(size = 4)

        // ASSERT
        assertEquals(listOf("1", "2", "Fizz", "4"), sequence)
    }

    @Test
    fun `buzz numbers`() {
        // ACT
        val sequence = fizzBuzz.generateSequence(size = 14)

        // ASSERT
        assertEquals(listOf(
                "1", "2", "Fizz", "4", "Buzz",
                "Fizz", "7", "8", "Fizz", "Buzz",
                "11", "Fizz", "13", "14"), sequence)
    }

    @Test
    fun `fizz buzz numbers`() {
        // ACT
        val sequence = fizzBuzz.generateSequence(size = 31)

        // ASSERT
        assertEquals(listOf(
                "1", "2", "Fizz", "4", "Buzz",
                "Fizz", "7", "8", "Fizz", "Buzz",
                "11", "Fizz", "13", "14", "FizzBuzz",
                "16", "17", "Fizz", "19", "Buzz",
                "Fizz", "22", "23", "Fizz", "Buzz",
                "26", "Fizz", "28", "29", "FizzBuzz",
                "31"), sequence)
    }

}
Enter fullscreen mode Exit fullscreen mode

If we run the tests, they should still pass. As our next step, we should take a careful look if there is any duplication in the production code.

And there is some!

The checks for “fizziness” and “buzziness” are duplicated between “fizzbuzz” case handler and corresponding “fizz” or “buzz” case handler. Simplest refactoring we can do is to extract these into private functions:

private fun generateNumber(number: Int): String {
    if (isFizz(number) && isBuzz(number)) {
        return "FizzBuzz"
    }

    if (isFizz(number)) {
        return "Fizz"
    }

    if (isBuzz(number)) {
        return "Buzz"
    }

    return number.toString()
}

private fun isBuzz(number: Int) = number % 5 == 0

private fun isFizz(number: Int) = number % 3 == 0
Enter fullscreen mode Exit fullscreen mode

At that point, you were about to refactor some more, for example, extract magic strings to constants.

But the interviewer told you it is enough refactoring.

You see they are impressed. You have finished earlier after all. But you feel as if the interviewer is a little bit suspicious. It is a classic exercise after all.

So they decide to give you an additional requirement!

A number should also be “Fizz” if it contains digit “3.”

A number should also be “Buzz” if it contains digit “5.”

A number should also be “FizzBuzz” if it contains both.

– anonymous interviewer

Oh, no!

Nobody told you there could be more…

Don’t fret! We are going to go through that. Let’s add a new test for the “Fizz” part with the new requirement.

We are going to generate the sequence until 32. Some numbers, which weren’t “Fizz” before, will become such:

@Test
fun `fizz numbers that just contain digit '3'`() {
    // ACT
    val sequence = fizzBuzz.generateSequence(size = 32)

    // 13 becomes Fizz
    // 23 becomes Fizz
    // 31 becomes Fizz
    // 32 becomes Fizz

    // ASSERT
    assertEquals(listOf(
            "1", "2", "Fizz", "4", "Buzz",
            "Fizz", "7", "8", "Fizz", "Buzz",
            "11", "Fizz", "Fizz", "14", "FizzBuzz",
            "16", "17", "Fizz", "19", "Buzz",
            "Fizz", "22", "Fizz", "Fizz", "Buzz",
            "26", "Fizz", "28", "29", "FizzBuzz",
            "Fizz", "Fizz"), sequence)
}
Enter fullscreen mode Exit fullscreen mode

Of course, the test is failing.

To make it pass we will need to modify the function isFizz appropriately. You should include the check for “it contains digit 3” on top of the original condition. In boolean logic (last time I checked) it was an “OR” operation between those two conditions:

private fun isFizz(number: Int) =
        number % 3 == 0 || number.toString().contains('3')
Enter fullscreen mode Exit fullscreen mode

Note the number.toString().contains('3') part. The test is passing now. But a few of other tests are failing!

Oh no!

We will have to fix those tests. We should replace numbers containing digit “3” with “Fizz”:

@Test
fun `buzz numbers`() {
    // ACT
    val sequence = fizzBuzz.generateSequence(size = 14)

    // ASSERT
    assertEquals(listOf(
            "1", "2", "Fizz", "4", "Buzz",
            "Fizz", "7", "8", "Fizz", "Buzz",
            "11", "Fizz", "Fizz", "14"), sequence)
}
Enter fullscreen mode Exit fullscreen mode
@Test
fun `fizz buzz numbers`() {
    // ACT
    val sequence = fizzBuzz.generateSequence(size = 31)

    // ASSERT
    assertEquals(listOf(
            "1", "2", "Fizz", "4", "Buzz",
            "Fizz", "7", "8", "Fizz", "Buzz",
            "11", "Fizz", "Fizz", "14", "FizzBuzz",
            "16", "17", "Fizz", "19", "Buzz",
            "Fizz", "22", "Fizz", "Fizz", "Buzz",
            "26", "Fizz", "28", "29", "FizzBuzz",
            "Fizz"), sequence)
}
Enter fullscreen mode Exit fullscreen mode

Hold on a second… Doesn’t this feel weird to you?

Yeah, I think this last case covers all our edge cases. That is because it (kind of) includes all other tests. So you should get rid of those tests!

Oh, and don’t forget to demote the private field fizzBuzz back to the local variable.

Here we go:

import org.junit.Assert.*
import org.junit.Test

@Suppress("FunctionName")
class FizzBuzzTest {

    @Test
    fun `generating fizz buzz sequence`() {
        // ARRANGE
        val fizzBuzz = FizzBuzz()

        // ACT
        val sequence = fizzBuzz.generateSequence(size = 32)

        // ASSERT
        assertEquals(listOf(
                "1", "2", "Fizz", "4", "Buzz",
                "Fizz", "7", "8", "Fizz", "Buzz",
                "11", "Fizz", "Fizz", "14", "FizzBuzz",
                "16", "17", "Fizz", "19", "Buzz",
                "Fizz", "22", "Fizz", "Fizz", "Buzz",
                "26", "Fizz", "28", "29", "FizzBuzz",
                "Fizz", "Fizz"), sequence)
    }

}
Enter fullscreen mode Exit fullscreen mode

Now we want to implement final two requirements.

For that let’s change our test to go until 53, so we hit more attractive edge cases. And some of the numbers will become “Buzz” and some other numbers ought to become “FizzBuzz” now.

See for yourself:

@Test
fun `generating fizz buzz sequence`() {
    // ARRANGE
    val fizzBuzz = FizzBuzz()

    // ACT
    val sequence = fizzBuzz.generateSequence(size = 53)

    // 35 became FizzBuzz
    // 51 became FizzBuzz
    // 52 became Buzz
    // 53 became FizzBuzz

    // ASSERT
    assertEquals(listOf(
            "1", "2", "Fizz", "4", "Buzz",
            "Fizz", "7", "8", "Fizz", "Buzz",
            "11", "Fizz", "Fizz", "14", "FizzBuzz",
            "16", "17", "Fizz", "19", "Buzz",
            "Fizz", "22", "Fizz", "Fizz", "Buzz",
            "26", "Fizz", "28", "29", "FizzBuzz",
            "Fizz", "Fizz", "Fizz", "Fizz", "FizzBuzz",
            "Fizz", "Fizz", "Fizz", "Fizz", "Buzz",
            "41", "Fizz", "Fizz", "44", "FizzBuzz",
            "46", "47", "Fizz", "49", "Buzz",
            "FizzBuzz", "Buzz", "FizzBuzz"), sequence)
}
Enter fullscreen mode Exit fullscreen mode

To make this pass, we will need to add similar condition to the isBuzz private function. Notice the number.toString().contains('5') part.

private fun isBuzz(number: Int) =
        number % 5 == 0 || number.toString().contains('5')
Enter fullscreen mode Exit fullscreen mode

The tests are back to green now (they pass).

Don’t you notice how isBuzz and isFizz functions have a similar structure? Don’t you think we should be able to extract that structure?

Let’s do it!

First, we will extract dividesBy(number, divider) private function:

private fun isBuzz(number: Int) =
        dividesBy(number, 5) || number.toString().contains('5')

private fun isFizz(number: Int) =
        dividesBy(number, 3) || number.toString().contains('3')

private fun dividesBy(number: Int, divider: Int) =
        number % divider == 0
Enter fullscreen mode Exit fullscreen mode

Second, we will extract containsDigit(number, digit) private function.

private fun isBuzz(number: Int) =
        dividesBy(number, 5) || containsDigit(number, 5)

private fun isFizz(number: Int) =
        dividesBy(number, 3) || containsDigit(number, 3)

private fun containsDigit(number: Int, digit: Int) =
        number.toString().contains(digit.toString())
Enter fullscreen mode Exit fullscreen mode

Note how digit here is not a character or string. It is an integer.

That way, we should be able to extract the whole structure (dividesBy or containsDigit) out of those two functions.

Here we go:

private fun isBuzz(number: Int) =
        dividesOrContainsDigit(number, 5)

private fun isFizz(number: Int) =
        dividesOrContainsDigit(number, 3)

private fun dividesOrContainsDigit(number: Int, digit: Int) =
        dividesBy(number, digit) || containsDigit(number, digit)
Enter fullscreen mode Exit fullscreen mode

Your interviewer is not stopping your refactoring yet. So maybe he is out of requirements?

Thank goodness!

Finally, you can do that magic strings extraction refactoring! For that, we will extract “Fizz” and “Buzz” strings out of respective edge cases. And from the “fizzbuzzy” one too:

companion object {
    private val FIZZ = "Fizz"
    private val BUZZ = "Buzz"
    private val FIZZ_BUZZ = "$FIZZ$BUZZ"
}

private fun generateNumber(number: Int): String {
    if (isFizz(number) && isBuzz(number)) {
        return FIZZ_BUZZ
    }

    if (isFizz(number)) {
        return FIZZ
    }

    if (isBuzz(number)) {
        return BUZZ
    }

    return number.toString()
}
Enter fullscreen mode Exit fullscreen mode

So why are we extracting these magic strings, anyway?

You might ask that yourself…

Or is it just the interviewer asking you that question?

Probably, because you want to make it easy to change one of these strings in just one place, don’t you?

And that is where the thought strikes you! What if I need to change “Fizz” to “Fizzy”?

Yep.

I’ll have to fix all these occurrences in the test suite.

Annoying!

Let’s replace all the magic strings in the test too. We will have to promote these private constants FIZZ, BUZZ, and FIZZ_BUZZ to internal access level so that that the test can access them:

companion object {
    internal val FIZZ = "Fizz"
    internal val BUZZ = "Buzz"
    internal val FIZZ_BUZZ = "$FIZZ$BUZZ"
}
Enter fullscreen mode Exit fullscreen mode

And finally, we are going to replace the string occurrences in the test suite:

import FizzBuzz.Companion.BUZZ
import FizzBuzz.Companion.FIZZ
import FizzBuzz.Companion.FIZZ_BUZZ
import org.junit.Assert.*
import org.junit.Test

@Suppress("FunctionName")
class FizzBuzzTest {

    @Test
    fun `generating fizz buzz sequence`() {
        // ARRANGE
        val fizzBuzz = FizzBuzz()

        // ACT
        val sequence = fizzBuzz.generateSequence(size = 53)

        // ASSERT
        assertEquals(listOf(
                "1", "2", FIZZ, "4", BUZZ,
                FIZZ, "7", "8", FIZZ, BUZZ,
                "11", FIZZ, FIZZ, "14", FIZZ_BUZZ,
                "16", "17", FIZZ, "19", BUZZ,
                FIZZ, "22", FIZZ, FIZZ, BUZZ,
                "26", FIZZ, "28", "29", FIZZ_BUZZ,
                FIZZ, FIZZ, FIZZ, FIZZ, FIZZ_BUZZ,
                FIZZ, FIZZ, FIZZ, FIZZ, BUZZ,
                "41", FIZZ, FIZZ, "44", FIZZ_BUZZ,
                "46", "47", FIZZ, "49", BUZZ,
                FIZZ_BUZZ, BUZZ, FIZZ_BUZZ), sequence)
    }

}
Enter fullscreen mode Exit fullscreen mode

All your tests should be green at this point.

Did you need to do all these refactorings?

Not necessarily.

Do you need to know how to do them, so that you are ready for any requirement change?

Absolutely!

Here is the final production code:

class FizzBuzz {

    companion object {
        internal val FIZZ = "Fizz"
        internal val BUZZ = "Buzz"
        internal val FIZZ_BUZZ = "$FIZZ$BUZZ"
    }

    fun generateSequence(size: Int): List<String> {
        return (1..size).map { generateNumber(it) }
    }

    private fun generateNumber(number: Int): String {
        if (isFizz(number) && isBuzz(number)) {
            return FIZZ_BUZZ
        }

        if (isFizz(number)) {
            return FIZZ
        }

        if (isBuzz(number)) {
            return BUZZ
        }

        return number.toString()
    }

    private fun isBuzz(number: Int) =
            dividesOrContainsDigit(number, 5)

    private fun isFizz(number: Int) =
            dividesOrContainsDigit(number, 3)

    private fun dividesOrContainsDigit(number: Int, digit: Int) =
            dividesBy(number, digit) || containsDigit(number, digit)

    private fun containsDigit(number: Int, digit: Int) =
            number.toString().contains(digit.toString())

    private fun dividesBy(number: Int, divider: Int) =
            number % divider == 0

}
Enter fullscreen mode Exit fullscreen mode

I genuinely thank you for reading, and I encourage you to practice your basic foundations.

There are tons of different challenges for that on websites like http://codingdojo.org/kata/ and http://kata-log.rocks. Huge thank you to creators and maintainers!

Liked this article? – Share it with your friends, on social media, hacker news, Reddit, etc. If you are interested in Kotlin, check out more of my stuff on Kotlin.

Top comments (9)

Collapse
 
tbodt profile image
tbodt

This is scarily close to being overengineered, IMHO

Collapse
 
waterlink profile image
Alex Fedorov

I guess that is why I add at the end:

Did you need to do all these refactorings?

Not necessarily.

Do you need to know how to do them so that you are ready for any requirement change?

Absolutely!

I would probably not do the Strings extraction here. At least, not until there is a feature on the top of backlog that requires it.

With the method extraction, I would probably leave it at the first level methods extraction (isFizz and isBuzz, without others). Again, until there is a feature that will make these other private methods handy.

As with any practice code, you want to take things to the max and extreme. You do that to see how things could be. And think when you would and would not do certain things (like I did in previous 2 paragraphs here).

Essentially, doing that expands your arsenal, when practicing and learning.

Collapse
 
jmfayard profile image
Jean-Michel 🕵🏻‍♂️ Fayard

I can very easily write a fizzbuzz implementation that pass all your tests but is completly wrong. For example:

fun evilFizzBuzzValidAccordingToTests(number: Int): String = when {
    number > 100 -> "FOOBAR"
    number % 3 == 0 && number % 5 == 0 -> "FizzBuzz"
    number % 3 == 0 -> "Fizz"
    number % 5 == 0 -> "Buzz"
    else -> number.toString()
}

So I would throw in properties based testing, here with kotlin test

import io.kotlintest.specs.FreeSpec
import io.kotlintest.specs.StringSpec



fun evilFizzBuzz(number: Int): String = when {
    number > 100 -> "FOOBAR"
    number % 3 == 0 && number % 5 == 0 -> "FizzBuzz"
    number % 3 == 0 -> "Fizz"
    number % 5 == 0 -> "Buzz"
    else -> number.toString()
}

fun realFizzBuzz(number: Int): String = when {
    number % 3 == 0 && number % 5 == 0 -> "FizzBuzz"
    number % 3 == 0 -> "Fizz"
    number % 5 == 0 -> "Buzz"
    else -> number.toString()
}


class FizzBuzzTesting : FreeSpec() { init {

    "it's always possible to trumps example-based testing" {

        val expected = listOf("FizzBuzz",
                "1", "2", "Fizz", "4", "Buzz",
                "Fizz", "7", "8", "Fizz", "Buzz",
                "11", "Fizz", "13", "14", "FizzBuzz",
                "16", "17", "Fizz", "19", "Buzz",
                "Fizz", "22", "23", "Fizz", "Buzz",
                "26", "Fizz", "28", "29", "FizzBuzz",
                "31")
        for (i in 1..expected.lastIndex) {
            evilFizzBuzz(i) shouldBe expected[i]
        }
    }

    "but no implementation can break properties-based testing" - {

        val fizzbuzz = ::realFizzBuzz
//        val fizzbuzz = ::evilFizzBuzz

        "Multiples of 15" {
            forAll<Int> { i ->
                if (Math.abs(i)  >= Integer.MAX_VALUE / 15 -1) return@forAll true
                val nb = 15 * Math.abs(i)
                fizzbuzz(nb) == "FizzBuzz"
            }
        }

        "Multiples of 3" {
            forAll<Int> { i ->
                if (Math.abs(i)  >= Integer.MAX_VALUE / 3 -1) return@forAll true
                val nb = 3 * Math.abs(i)
                nb % 5 == 0 || fizzbuzz(nb) == "Fizz"
            }
        }

        "Multiples of 5" {
            forAll<Int> { i ->
                if (Math.abs(i) >= Integer.MAX_VALUE / 5 -1) return@forAll true

                val nb = 5 * Math.abs(i)
                nb % 5 == 0 || fizzbuzz(nb) == "Fizz"
            }
        }

        "Others" {
            forAll<Int> { i ->
                val nb = Math.abs(i)
                nb % 5 == 0 || nb % 3 == 0 || fizzbuzz(nb) == nb.toString()
            }
        }

    }


} }
Collapse
 
waterlink profile image
Alex Fedorov

Yes. You can.

But will you, really?

IMHO alert.

In Test-Driven Development, you pass the failing test with the simplest possible solution. And your solution has a higher complexity than the one proposed in the article.

You have one edge case handler more (additional “if” statement).

Therefore, if TDD practiced properly, such “evil” solution will not be written.

Anyways.

I agree that PBT has a higher degree of correctness when it comes to testing. Don’t forget, though, that EBT is easier to read and reason about.

I mean, look at that:

         "Multiples of 15" {
            forAll<Int> { i ->
                if (Math.abs(i)  >= Integer.MAX_VALUE / 15 -1) return@forAll true
                val nb = 15 * Math.abs(i)
                fizzbuzz(nb) == "FizzBuzz"
            }
        }

There is just so much stuff involved here. Show that to an apprentice-level developer and they will have a lot of questions about this test. It is not simple.

Collapse
 
jmfayard profile image
Jean-Michel 🕵🏻‍♂️ Fayard

For this "Multiples of 15" test, my refactored version would be something like

         "Multiples of 15" {
            forAll(BigInteger.positive()) { nb ->
                fizzbuzz(15 * nb) == "FizzBuzz"
            }
        }

As you can see one small pb was that I was too lazy to lookup how to test for only positive numbers. The harder and much more interesting one is I'm not used to think about integer overflows. This style of testing forced me to think about it because my test kept failing! That was actually a teachable lesson.

Anyway I highly recommend this, most fun presentation about testing I have ever read!

slideshare.net/ScottWlaschin/an-in...

Thread Thread
 
waterlink profile image
Alex Fedorov

Great refactoring!

Still the "15 * nb" part requires a mental leap.

Example tests are nice because, when written well, they can be read very quickly (like prose).

Another thing. So what if I don't need my code to handle overflows? Because all the numbers that business wants to work with are under 500.

YAGNI in that case.

Thread Thread
 
waterlink profile image
Alex Fedorov

I've taken a look at the presentation. And I'm still convinced that 2 tests is enough to drive out correct "add" code if you follow 3 rules of TDD.

And if you doubt that I thought about this enough, here is my own presentation about PBT: github.com/waterlink/property-base...

Collapse
 
waterlink profile image
Alex Fedorov

Another way to look at this “simplest possible way to make the failing test pass” is to imagine you have a developer on the team, who is removing any code that is not tested.

So that additional if statement is not tested by the test. That means it can be safely removed.

You can apply such radical technique to teams that do TDD rigorously. And if you knew there is a person on your team (and that person – is every member of the team), you are probably going to make sure to have all the production code covered.

You just don’t add any code that is not tested, as simple as that.

Collapse
 
kenvy profile image
zw • Edited

Simple but a very good teachable tdd article.

Look forward to your IOS version, Kotlin series of learning articles.