Trying to adapt "remove redundancy" step of TDD in Julia from Java example

I’ve become fond of the idea of TDD and started implementing a few ideas in Julia. The weird part about it is that most material is focused on class-based OOP ideas implemented in Java. This ends up creating a bit of confusion when trying to translate the ideas and steps taken by the authors.
Q1: do you have any recommended literature on TDD+Julia from experienced programmers?

Specifically, I’ve been reading Test Driven Development By Example by Kent Beck. By page 28, he has three classes: Money, Dollar and Franc as follows (code will speak better than me):

class Money{
    protected int amount;
class Dollar extends Money{
class Franc extends Money{

The first problem in adaptation is Julia doesn’t really recommend inheritance (and doesn’t support class-based OOP).
My adaptation so far is:

abstract type Money end

struct Dollar <: Money

struct Franc <: Money

Q2: As far as I understand, there’s no recommended way to remove that duplicated “amount”, is there? Aside from some sort of composition or @forward macro, as I saw in the big, old thread. (Julia hadn’t even reached 1.0 by then). Composition and inheritance: the Julian way - #59 by favba

Lastly, multiple dispatch seems to end up with a bunch of duplicate code, since there’s no super or similar. For example, I have these

times(m1::Dollar, multiplier::Int) = Dollar(m1.amount * multiplier)
times(m1::Franc, multiplier::Int) = Franc(m1.amount * multiplier)

equals(m1::Money, m2::Money) = m1.amount == m2.amount

equals is fine, but times is getting duplicated since it has to call a specific constructor for that specific type. I’ve read a lot these last days and I found a generic way to implement times:

times(m1::Money, multiplier::Int) = typeof(m1)(m1.amount * multiplier)

From my understanding, there’s no way an object is created as ::Money, so the typeof(m1)() instruction will always generate a valid concrete constructor.
so… Q3: is that correct?

I’d love to learn more about “The Julian Way of OOP”. If you reached this point, thank you very much!

1 Like

There is:

times(m1::T, multiplier::Int) where {T <: Money} = T(m1.amount * multiplier)

Edit: even better would be adding a method to *:

Base.:*(m1::T, multiplier::Int) where {T <: Money} = T(m1.amount * multiplier)

In general, TDD is not about OOP, but about writing unit tests of your interfaces before actually implementing them. This can be done with any programming language / paradigm.

Edit: a cool (experimental) new approach to tests / TDD is here: GitHub - JuliaPluto/PlutoTest.jl: 🎈 Visual, reactive testing library for Julia. Time machine included.

1 Like

I know that this is only a toy example, but in real world I would not create a separate class (Java) or struct (Julia) for each currency, but one type for the currency information and one type for currency amounts (amount + currency).
In Julia you could even put the currency information into type domain, so that it has zero impact on runtime performance.
Example: GitHub - lungben/CurrencyAmounts.jl

Thanks! It’s so hard to find that syntax in proper references. It makes perfect sense :slight_smile:

It doesn’t help that the book is not in public domain, but its intention is to show TDD practices as slow as possible and incrementally take bigger steps. It uses Java+COOP because it’s the standard for the author (that’s why I’m wondering if there’s literature on Julia TDD, since we don’t use class-based OOP).

Thanks for the extra links, I’ll take a careful look at them. I tried really hard to be inspired by the type system in Base, but some definitions or constructors are spread across multiple source files, and sometimes there’s a cascade of method calls for safety.

but in real world I would not create a separate class (Java) or struct (Julia) for each currency, but one type for the currency information and one type for currency amounts (amount + currency)

The book itself reaches that conclusion in the following chapter. “By this point, since we removed duplication, Dollar and Franc are useless”, then proceeds to slowly remove them, driven by tests ^^.

p.s.: since you didn’t comment on it, it seems that “amount” has to be present in both structs, for this pet example.

1 Like

I still haven’t read the TDD repository, but I took a look at your CurrencyAmounts.jl and Currencies.jl. Firstly, I think it’s very interesting how the concepts in the book ended up converging in some aspects and diverging in others. For case studied, the main goal we were trying to reach was add 5 USD + 10 CHF with a fictional exchange rate of 2:1 CHF->USD and reach 10 USD as a result. Everything done with simple integer operations, no operator overload (in this case special dispatch cases) and no other currencies, etc.

I tried my best to follow the structure from the book’s code so that the one could make parallels between the Java implementation and my attempt at Julia + classless OOP. I’m happy with the result, since I had to adapt a few constructs and find my own solutions driven by tests (where the book would, for example, say “create a class” or “a new interface” and I had to be creative).

I’ve uploaded the full history on my github, since sharing the code here would have 50+ lines of spam. This is the direct link for the equivalent runtests.jl.

Interesting finds:

  • My version has 61 lines of functionality and 101 lines of test code. The book has 91 lines of functionality and 89 lines of tests.
    • The number of lines in both testbenches can be reduced, specially on the setup of the last tests, but I aimed to be as close as the book was
  • The Expression analogy used by the author is pretty interesting. When $5 + $5 happens, the obvious solution is $10. When different currencies appear, his approach was to create an impostor Expression (which in Java was an interface, but more experienced programmers may translate the idea better on Julia), and have Sum represent the operation. In his words, it’s similar to how “(2 + 3) * 5” can be saved as an expression and not solved/evaluated until a “reduce” command is explicitly called.
  • The code, as the example explains, is nowhere near complete, but it provides a nice, gentle introduction to TDD and how to add features by adding the tests we desire to pass (and how we desire to solve them in an interface level)

I’m open for suggestions on missed oportunities in the Julia adaptation and some obvious architectural choices that I didn’t do because I was explicitly trying to mimick Java (like not using parametric structs and not using this rich part of Julia)

1 Like

I went back and forth into PlutoTest. From what I understand, you need your code to be inside the notebook so it reacts to any change and re-runs the tests automatically. One problem is having your code in ipynb (or similar, if it’s not a “jupyter notebook” format), instead of jl, and then having all the code you want to test inside it.

Something that I think is extremely useful either way is being able to go step by step on the code using the slider until it reaches the evaluation point. This can be useful even if you don’t have the notebook being reactive (i.e.: import your .jl into it and manually prompt a run on all tests)

This makes me believe there could be an alternate solution which would take the form of an IDE extension (either for VS Code or Juno, for example), where you’d work with .jl files, but you’d be able to get this reactive behavior.

That said, I’m always impressed by the works of the Julia community! PlutoTest seems like a huge step towards easier testing.

1 Like

In contrast to Jupyter, Pluto files are plain Julia files (just with some additional comments), therefore you can directly include them as source code in your package.
An example is in the Pluto source code itself: Pluto.jl/Firebasey.jl at main · fonsp/Pluto.jl · GitHub

If the tests are fast, they could stay in the source code itself (a few ms longer load time for a package is in most cases not an issue). Longer tests should be moved to a separate file (could also be a Pluto notebook) and included in runtests.jl.

You could develop a package in VSCode and have the tests in Pluto, using Revise.
When updating the package in VSCode, you have to manually re-run the import statement in the Pluto notebook, triggering a re-run of all tests.
The drawback is that all tests are re-run for any change, when you develop and test in the same Pluto notebook it only re-runs tests affected by the code change.

1 Like

I’m really eager to have PlutoTest be a part Julia flow design/coding flow, but I have no idea how to set up the pieces. I usually have 1 Julia REPL, 1 bare terminal (bash) and 1 text editor (in this case VS Code). The bare terminal is basically running julia runtests.jl. It can probably be substituted for Pluto, but in this case I’d have to have the source cade and the tests in the same file, is that correct?

The description makes me believe I need them in the same file, but maybe I can simply use an import/using statement to get the same functionality but work on different files.

I haven’t tried it myself, but I would replace the bare terminal with a Pluto notebook. That notebook should import your functionality via using Revise and using (your modules) or includet(your files). Then you can write tests in the pluto cells which should only be re-run when relevant code changes.

1 Like

In the case of Firebasey.jl that you linked. It has definitions and @tests. I don’t know the structure of Pluto, so I don’t know where to start looking for where it calls the file. Certainly they don’t include it, because that would mean running all tests in that process.

So they must have a module/package wrapper in which they export the appropriate functions from the top module/package/file, right?

I’m starting to like even more this idea where source code and unit tests are in the same file (for a documentation point of view), and any other sorts of stress tests can be in their own dedicated folders and source code.

It is included in the Pluto source code: Pluto.jl/Dynamic.jl at cd7c123e5dc9cb5c2d28e119aa66b83cdb0371f7 · fonsp/Pluto.jl · GitHub
If the tests are fast, they are not slowing down import of Pluto significantly (importing Pluto takes ~1s on my machine). In contrast, these tests may trigger compilation of specific functionality which may be required later, and a slightly longer import time may be better than a lag when using Pluto itself.

1 Like