DEV Community

Discussion on: We must deliver bug-free code before someone kills a thousand people and governments legislate coding!!

Collapse
 
jayjeckel profile image
Jay Jeckel • Edited

No worries, I was being hyperbolic calling it "fearmongering" and was not actually afraid. You need not worry for several reasons. First, in some nations software engineers are already a registered and certified profession, similar to doctors, lawyers, and other engineering professions. Second, people have died from faulty code and this has led to certain industries requiring greater oversite on software production. For example, see the Therac-25 incident where medical software bugs led to greater regulation in that industry. The point being, people have died and it hasn't led to over regulation of the entire coding industry.

To answer your question, I keep my code maintainable with unit and other tests, I just don't follow the tenets of TDD. Unit testing is a marvel that everyone should use, but TDD itself isn't needed.

Always writing tests first and all the other cargo code of TDD are just rituals that, hopefully, get devs to use unit tests. But those rituals don't lead to better tests than writing tests after a feature is already implemented and in many cases they provide worse tests as TDD tends toward testing implementation instead of testing features.

If you want specifics, I design a feature, gather its requirements and public surface API, implement the feature according to the reqs and API design, then I write tests for the feature based on the reqs and API design. The tests are then decoupled and only test the feature not its implementation. In some cases it has been two different developers, one doing implementation and the other doing the tests, because both gather their information from the design.

Thread Thread
 
ultrabugbestof1 profile image
Ultrabugbestofplus

Of course i'm not the best, I'm relatively new to TDD (only 2 professional projects with)

Okay so in your approach, you decouple the feature from its adapters (db, web framework, etc.), and then you write its spec in the form of tests, then you write the adapters and test those as well. I could live with that.

There is a misconception about how you understand tho TDD: It's not about testing implementation rather than feature and not about writing better tests. It's just a tool to make designs emerge through tests. BTW the name "Test Driven Dev" is wrong in my opinion; it should be "Making clean code through rounds of refactoring with no fear of regression because of tests". So MCCTRORWNFORBOF for short.

In my day to day work, the first thing I do Is I describe in one test the most simple case of the feature I'm implementing. I make then the code for the test to pass. Then I continue with the next case: test -> implementation -> refactor/cleaning and rinse and repeat. So, in the end, my tests reflect what is expected from the business logic I've implemented. So same result tho in some cases the approach of "writing your feature step by step" can lead to some cleaner results.

A stupid example to back my claims:

Say you are tasked to implement a feature that detect if a given input string is a palindrom.
So you implement the feature first and it would look something like this:

const isPalindrom = (str) => str === str.split('').reverse().join('')
Enter fullscreen mode Exit fullscreen mode

Now of course this isn't the most efficient way of doing this but it would satisfy the following tests:

isPalindrome(''); => true
isPalindrome('a'); => true
isPalindrome('ab'); => false
isPalindrome('aa'); => false
isPalindrome('aba'); => true
isPalindrome('sugus yay sugus'); => true
isPalindrome('Js is fun'); => false
Enter fullscreen mode Exit fullscreen mode

So now according to your methodology i'm done with it. And yes its good enough. I can rewrite this better by iterating the string with two indexes: one starting at the start and one starting at the end and check if the characters at both indexes are the same. Better implementation, more efficient in terms of space and speed cause you can early return.

Now an experienced dev knows that ofc but for a beginner probably won't go further than the first impl.

In TDD tho you would test the first case: ""

const isPalindrom = (str) => true;
Enter fullscreen mode Exit fullscreen mode

Now "a"

const isPalindrom = (str) => true;
Enter fullscreen mode Exit fullscreen mode

Now "ab"

const isPalindrom = (str) => {
    if (str.length < 1) {
        return true;
    }
    return str[0] === str[1];
}
Enter fullscreen mode Exit fullscreen mode

Now "aba"

const isPalindrom = (str) => {
    if (str.length < 1) {
        return true;
    }
    for (let i = 0, j = str.length - 1; i <= j; i++, j--) {
        if (str[i] !== str[j]) {
            return false;
        }
    }
    return true;
}
Enter fullscreen mode Exit fullscreen mode

Now with more advanced examples + some cleaning:

const isPalindrom = (str) => {
    for (let i = 0, j = str.length - 1; i <= j; i++, j--) {
        if (str[i] !== str[j]) {
            return false;
        }
    }
    return true;
}
Enter fullscreen mode Exit fullscreen mode

So it forces me to decompose my feature and solve it little by little leading often enough to more clever designs. Tho for this simple example its a bit simplistic. A real use case was when I was task to write and app that would transcribe then translate then speech to text then glue speech to text and translation back on the original video. Here was some of the class that was used to schedule work:

public class PipelineRunnerUseCase {
    ...

    public void submitToExecutor(final List<Task<?>> tasks) {
        validateListNotEmpty(tasks);
        validateAtLeastOneEntrypointInGraph(tasks);
        validateNoLoopOnTask(tasks);
        validateTaskNameUniqueness(tasks);
        validateAndSubmitDirectedAcyclicExecutionGraph(tasks);
    }

    public void onTaskCompletedCallback(
            final String graphId,
            final Task<?> completedTask
    ) {
        final DirectedAcyclicGraph graph = graphRepository.get(graphId);
        graph.updateVertexValue(completedTask);

        final List<Task<?>> next = graph.getDescendants(completedTask);
        if (next.isEmpty()) {
            final HeterogeneousContainer outputContainer = handleEndOfExecutionBranch(graphId, completedTask);
            if (graph.isFinished()) {
                onGraphCompletedCallback.accept(new Tuple2<>(graphId, outputContainer));
            }
        } else {
            scheduleNextTask(graphId, completedTask, next);
        }
        graphRepository.save(graphId, graph);
    }
...
Enter fullscreen mode Exit fullscreen mode

And some tests that were used:

...
 @Test
        @DisplayName("Should throw if the graph is empty")
        public void onEmptyGraph() {
            assertSutException(Collections.emptyList(), "Tasks list cannot be empty.");
        }

        @Test
        @DisplayName("Should throw if there is only one node in the graph and its not an input (no inlets).")
        public void onGraphWithOneNodeWithAnIndependentNode() {
            assertSutException(
                    List.of(Task.<Void>createNotStarted(
                            List.of("In"),
                            "TestNode",
                            (c) -> null
                    )),
                    "A graph cannot be without any entrypoint (at least one task without inlets)."
            );
        }

        @Test
        @DisplayName("Should throw if a node cycle on itself.")
        public void onTaskSelfCycle() {
            assertSutException(
                    List.of(
                            Task.<Void>createNotStarted(
                                    Collections.emptyList(),
                                    "TestNode",
                                    (c) -> null
                            ),
                            Task.<Void>createNotStarted(
                                    List.of("In", "TestNode2"),
                                    "TestNode2",
                                    (c) -> null
                            )
                    ),
                    "A task cannot cycle onto itself."
            );
        }
...
Enter fullscreen mode Exit fullscreen mode

See? Here i'm testing the feature not hard testing the impl my feature being to be able to schedule inter-dependent tasks either locally or in a distributed manner.