Test-Driven vs REPL-Driven Development

Reflections on the advantages of a TDD and REPL based approach to software development

REPL-Driven development is all about moving fast with rapid iterations towards the goal.
function assemble(t::Thingy, d::Doodad, s::Stuff)
bolts = get_bolts(HEX, 4)
goo = get_glue(SUPER)

gizmo = put_together(flip(t), s, bolts)
tighten!(gizmo)

doohickey = glue(gizmo, d, goo)
dry!(doohickey)
if isfaulty(doohickey)
throw(AssemblyFailed("Faulty parts, buy new ones!")
end
return doohickey
end
function assemble(t::Thingy, d::Doodad, s::Stuff)
bolts = get_bolts(FLANGED, 10)
goo = get_glue(EXPOXY)
gizmo = glue(t, s, goo) # wrong assumption on input/output
dry!(gizmo)
doohickey = put_together(gizmo, d, bolts)
tighten!(doohickey)
return doohickey
end

The Test-Driven Approach

The test-driven approach to this is to write a unit test for the assemble function. In fact with TDD, you write the test before you even define an empty function.

const thing = Thingy()
const dud = Doodad("qwerty")
const stuff = Stuff(42)
@testset "Test if valid object created" begin
gizmo = assemble(thing dud, stuff)
@test !isnothing(gizmo)
end
function assemble(t::Thingy, d::Doodad, s::Stuff)
return Gizmo()
end

A Problem with Test-Driven Development

With test-driven development we are running tests over and over again making changes repeatedly until all our tests succeed. Here is an example from an actual Julia package I wrote, when I deliberately introduced failure:

(LittleManComputer) pkg> test
Testing LittleManComputer
Status `/private/var/folders/n7/v31m12_x6lj1qpqrbsg7ln300000gn/T/jl_73BcOb/Project.toml`
[c742fd3c] LittleManComputer v0.1.0 `~/Development/Julia/LittleManComputer`
[8dfed614] Test
Status `/private/var/folders/n7/v31m12_x6lj1qpqrbsg7ln300000gn/T/jl_73BcOb/Manifest.toml`
[c742fd3c] LittleManComputer v0.1.0 `~/Development/Julia/LittleManComputer`
[2a0f44e3] Base64
[8ba89e20] Distributed
[b77e0a4c] InteractiveUtils
[56ddb016] Logging
[d6f4376e] Markdown
[9a3f8284] Random
[9e88b42a] Serialization
[6462fe0b] Sockets
[8dfed614] Test
Assemble mnemonic: Test Failed at /Users/erikengheim/Development/Julia/LittleManComputer/test/assem_tests.jl:45
Expression: assemble_mnemonic(["ADD", "12"]) == 111
Evaluated: 112 == 111
Stacktrace:
[1] top-level scope at /Users/erikengheim/Development/Julia/LittleManComputer/test/assem_tests.jl:45
[2] top-level scope at /Users/julia/buildbot/worker/package_macos64/build/usr/share/julia/stdlib/v1.5/Test/src/Test.jl:1115
[3] top-level scope at /Users/erikengheim/Development/Julia/LittleManComputer/test/assem_tests.jl:41
[4] top-level scope at /Users/julia/buildbot/worker/package_macos64/build/usr/share/julia/stdlib/v1.5/Test/src/Test.jl:1115
[5] top-level scope at /Users/erikengheim/Development/Julia/LittleManComputer/test/assem_tests.jl:5
Test Summary: | Pass Fail Total
All Tests | 56 1 57
Assembler tests | 35 1 36
Symbol table | 8 8
Instruction set | 10 10
Regression test | 9 9
Assemble mnemonic | 8 1 9
Disassembler tests | 11 11
Simulator tests | 10 10
ERROR: LoadError: Some tests did not pass: 56 passed, 1 failed, 0 errored, 0 broken.
in expression starting at /Users/erikengheim/Development/Julia/LittleManComputer/test/runtests.jl:4
ERROR: Package LittleManComputer errored during testing

REPL Driven Development

REPL is short for read-evaluate-print-loop. It means an interactive terminal such as your Bash shell or DOS command line interface, where you type a command and see an immediate response. A command or expression is read and then evaluated. The result is then printed to screen.

julia> 2 + 3
5
julia> length("hello")
5
julia> map(sqrt, [9, 16])
2-element Array{Float64,1}:
3.0
4.0
julia> t = Thingy()
Thingy()
julia> d = Doodad("qwerty")
Doodad("qwerty")
julia> s = Stuff(42)
Stuff(42)
julia> bolts = get_bolts(FLANGED, 10)
10-element Array{FlangedBolt,1}:
FlangedBolt()
FlangedBolt()

FlangedBolt()

julia> goo = get_glue(EXPOXY)
EpoxyGlue()
julia> gizmo = glue(t, s, goo)
ERROR: MethodError: no method matching glue(::Thingy, ::Stuff, ::Glue)
Closest candidates are:
glue(::Gizmo, ::Doodad, ::Glue) at sticky-stuff.jl:538
julia> gizmo = glue(d, goo)
Doohickey(1331)
julia> 5 ÷ 2
2
julia> 5 % 2
1
gizmo = put_together(flip(t), s, bolts)

Rapid Iteration in Context

This is what I see as the major advantage of a REPL approach. You are rapidly iterating in context. With a TDD approach you don’t really know which line of code is the culprit, only the test that failed and in which function. Yes, sometimes you actually made a coding mistake which produce an error on the correct line. But that will require either scanning through the output from running the test or clicking on some error output that jumps you to the right spot in the code. Either way you need to deal with a lot of line noise.

julia> camel_case(s) = join(uppercasefirst.(split(s, '_')))
camel_case (generic function with 1 method)
julia> camel_case("hello_how_are_you")
"HelloHowAreYou"

How Does This Scale?

If this was all there was to it, then this would of course not scale. You cannot manually test functions in a REPL each time you make code changes in a larger project.

Objections to non-TDD Based Development

There are some obvious objections to this. The idea of TDD is that it forces you to think about how a function should behave first, thus avoiding that you accidentally start testing for the things you know works. The idea is also that by forcing you to think of tests first you are exposed to the edge cases and things you need to think about as you develop your code.

Fundamental Issue — Reading is Easier than Writing

I think an analogy is useful in expressing what I see as the fundamental problem of TDD. Anyone who has ever tried learning a foreign language should know that reading a language is substantially easier than writing it. Likewise listening to someone speak is easier than speaking yourself.

The Power of Prototyping and Iteration

This applies to numerous human endeavors. It is not without reason why rapid iteration and prototyping has been so successful in many fields. Experimenting and making simplified versions of what you intend to be the final product is a good way of developing a better understanding of what you are supposed to make.

The Intentions of My Advice

My intention here is not tell you to follow another fad. If you are successfully doing TDD, then there is no need to care all that much about what I am writing here. If it works for you, that is great. My intention is simply to explain why it may not work for everyone. Indeed maybe you are one of those who cannot seem to get it to work satisfactory for you. Hopefully I am able to put some words and observations about the same kinds of things you have experienced.

Geek dad, living in Oslo, Norway with passion for UX, Julia programming, science, teaching, reading and writing.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store