News for hackers. Stuff that matters.
Re:CUPID – the back story by agnosias on Sunday March 21, 2021 @12:37PM (#26531653)
Honest question: if you don't do dependency inversion, or if you don't depend on interfaces/abstractions that can't be mocked - how do you unit test your code?

Unit testing is the only reason pretty much all of my code depends on interfaces. Some people seem to consider this a bad thing/over-engineering, but it's how I've seen it done in every place I've worked at.

How do you do it?

Re:CUPID – the back story by aszen on Sunday March 21, 2021 @12:58PM (#26531842)
Firstly if the dependency isn't doing any io, you can test your code as a whole along with its dependency. No need to mock.

More interesting is if your code relies on the outside world, then instead of abstracting out the connection with the outside world abstract out your business logic and test it separately.

So instead of a database repository being injected into your domain services, make your services rely on pure domain objects which could come from any where be it tests or the database.

Make a thin outer shell which feeds data into your domain logic from the outside world and test that via integration tests if necessary.

I'll admit I don't have the full picture here, but I have used this technique to good effect. The core idea is don't embed your infrastructure code deep inside your architecture instead move it to the very top.

Re:CUPID – the back story by globular-toast on Sunday March 21, 2021 @01:12PM (#26531969)
> Firstly if the dependency isn't doing any io, you can test your code as a whole along with its dependency. No need to mock.

This the thing. People have a tendency to overuse mocks. The point of automated testing (whether it's unit tests or something else), is to enable refactoring. That's really the only reason. Code that you don't touch doesn't suddenly change its behaviour one day.

In a previous jobs the developers started to go crazy with mocking and it reached a kind of singularity where essentially if a function called anything, that thing was mocked. It definitely tests each function in complete isolation, but what's the point? It makes refactoring impossible, which is the entire point of the tests in the first place!

This excellent talk completely changed the way I approached testing. Every developer who writes tests needs to watch this now! https://www.youtube.com/watch?v=EZ05e7EMOLM

Re:CUPID – the back story by chakspak on Sunday March 21, 2021 @03:59PM (#26533416)
I generally agree with you, though I want to comment on this line:

> Code that you don't touch doesn't suddenly change its behaviour one day.

This can and does happen all the time, when the platforms and abstractions your code builds on change underneath you. This is why a compelling environment / dependency management story is so important. "Code rot" is real. =P

Re:CUPID – the back story by globular-toast on Sunday March 21, 2021 @04:16PM (#26533569)
I thought about this, but technically that is still the code changing, it just happens to be in your dependencies rather than your codebase. The only reason you really have to change dependency versions is security fixes, and they should be infrequent enough that you could do manual testing. So I don't think it's a compelling reason to write unit tests, although it is certainly an added value.
Re:CUPID – the back story by garethrowlands on Sunday March 21, 2021 @01:08PM (#26531934)
Some of this is covered in "Functional Core, Imperative Shell", https://www.destroyallsoftware.com/screencasts/catalog/funct...

Haskell programs tend to have this structure because pure functions aren't allowed to call impure functions.

Re:CUPID – the back story by snidane on Sunday March 21, 2021 @01:36PM (#26532190)
You are doing interface/function level testing and calling it unit testing.

That's what industry converged on, that a function/method = a unit, but it apparently used to be that a module meant to be a unit.

It can be that both interfaces and modules can be considered a "bag of functions".

Seems like a lot of confusion about unit testing, mocking and DI stems from this historical shift.

I believe that interface/method level testing is too granular and mostly results in overfitted tests. Testing implementation, not behaviour. Which can be useful for some algorithm package for example, but probably not so much applicable to typical business logic code.

TDD: where did it all go wrong. https://youtu.be/EZ05e7EMOLM

Re:CUPID – the back story by robertknight on Sunday March 21, 2021 @12:49PM (#26531770)
An option in some languages is to simply create an alternate mock/fake version of the actual class or function that is depended upon and monkey-patch the code under test to use it for the duration of the test. This is commonly done in Python ( see `unittest.mock.patch`) and JavaScript (eg. with Jest mocks) for example.

The end result is the same as if you'd created an interface or abstraction with two implementations (real and fake/mock), but you skip the part where a separate interface is defined.

The upside to this is that the code is more straightforward to follow. The downside is that the code is exposed/potentially coupled to the full API of the real implementation, as opposed to some interface exposing only what is needed.

Re:CUPID – the back story by mrloba on Sunday March 21, 2021 @12:53PM (#26531800)
Move your logic to pure functions. Use classes for plumbing. Now you can unit test the logic, and integration test the plumbing. You can easily test with a real database using docker for example. Note that you'll still use DI in this case, but you'll have far fewer classes and therefore fewer dependencies as well.
Re:CUPID – the back story by user_agent on Sunday March 21, 2021 @01:07PM (#26531919)
'How do you unit test your code without mocks?'

I strongly recommend reading "Unit Testing: Principles, Practices, and Patterns" by Vladimir Khorikov.

That's going to answer your question and will unveil a whole world of testing without mocking (not that it's better or worse than with mocks). It might be a single best book about unit testing in general.

Re:CUPID – the back story by geofft on Sunday March 21, 2021 @01:39PM (#26532220)
The good idea of unit testing is repeatable, reliable tests, ideally those that can be run as part of CI before every merge and also on your end-user machine. "Unit" testing imposes an architectural demand on how you do this.

What you actually want is fast and self-contained integration tests. Have an API endpoint that returns some filtered data from the DB? Spawn a DB server locally, load some fixtures into it (e.g., phrased in the form of a migration), start up your application server, and curl it and see if the data is filtered. Then shut it down. In most stacks you can do all of this in a fraction of a second.

The reason most shops love unit tests is that it saves them from having to figure out how to run those integration tests. It's easier from a Conway's Law perspective to use your language's built-in testing facilities to test the function you're writing, let the ops team figure out how one starts up a database server, and let the frontend team figure out how one makes HTTP requests to your application. But you will deliver better software if you spend a bit of time figuring out all those things and stick them in a tiny shell script.

Re:CUPID – the back story by jdlshore on Sunday March 21, 2021 @01:00PM (#26531861)
You can separate your code into “logic” which has no external dependencies (just logic, eg a domain model) and “infrastructure” which does. Infrastructure still requires DI but Logic doesn’t.

Some programs are nothing but Infrastructure (DB backed microservices) but some have complex Logic (complex problem domains). In the latter case the most important parts of the system can be tested without DI.

You still need DI but not pervasively.

Re:CUPID – the back story by drblast on Sunday March 21, 2021 @02:02PM (#26532430)
Most of the time I've seen dependency inversion "so we can test" the code is littered with hundreds of single-implementation interfaces and no mocks, or stub mocks that are written just to pass the tests. It doesn't actually test anything.

In the case where there are mocks, the unit tests are almost always literally testing "can my programming language call a function and return a result" or "can we store data in a database if the database works?" They are functional tests disguised as unit tests, and are testing only non-production fake functionality.

Write functional code instead. Liberally use the compiler and the type system to make mistakes impossible instead of unit testing.

Unit tests are for when you can't express something in a way where the compiler and type system will save you from errors.

Everything else is a functional or integration test and should be testing the production system, not a mockup of the production system that works differently.

Basically, write Haskell programs in whatever language you're working in. Use unit tests as a backstop for when the type system isn't as good as Haskell's.

Re:CUPID – the back story by ItsMonkk on Sunday March 21, 2021 @01:12PM (#26531975)
Mark Seemann's blog I've been reading for the past couple years answers this question. In particular, his posts on Hexagonal Architecture[0] and Dependency Rejection[1], but there are many more articles that cover this and I highly endorse reading.

To summarize, you want to set up your code into two distinct categories, data and pure functions. Your data is at the top level of your code, so if you need external data that needs to be at the top level of your module. You then feed that data through a pipeline of pure functions that return with some sort of answer back to your top level module that can then pass into some other function(or through a quick lambda) that does the actual transformation at the top level. The key is that a function can either be a pure function or it can be an impure function. Never both.

As an example, let's say you've got code that has to read from a couple text files. Currently your code does this through a couple nested loops, with the loops passing in what folder it is in and what its file name is, but your code is filtering through some that you don't want to read.

To re-envision this code, you would create a pure function that comes back to the top of your module of which files to read. Then that top module would read those files, and pass them to some other function that does transformations on the result.

You now don't have to create mocks, just create data and pass that into your pipeline and then check if it gets the expected result. One of the interesting side effects of doing this, is at a certain point you stop unit testing for correctness(as the code is so easy to reason about written in this style), and start unit testing primarily for documentation.

[0]: https://blog.ploeh.dk/2013/12/03/layers-onions-ports-adapter...

[1]: https://blog.ploeh.dk/2017/02/02/dependency-rejection/

Re:CUPID – the back story by jnwatson on Sunday March 21, 2021 @12:57PM (#26531835)
You pick a language that doesn’t require you to bend your architecture around testing.

This is a classic example of software-pattern-being-language-defect.

It is trivial to mock out upstream or downstream dependencies in Python, without any changes to the code under test.

Re:CUPID – the back story by madhadron on Sunday March 21, 2021 @01:42PM (#26532246)
People have already pointed out using pure functions for the heavy lifting and then making the IO as simple as possible, but another issue is tooling. If you have something like Standard ML's module system or COM style components, you define a functor/interface/contract that your code depends on, and then implement a test form and a production form.

This is basically what dependency injection is doing, but working around the fact that modular programming didn't really catch on.

Re:CUPID – the back story by stepbeek on Sunday March 21, 2021 @12:49PM (#26531772)
I’ve generally understood the criticism of DI to be levied at codebases where it was applied to literally every class. I’m not the author, but I don’t imagine that relying on interfaces to abstract between layers of an application or to avoid a concrete dependency on something like an external api to be what he was arguing against.
Re:CUPID – the back story by aszen on Sunday March 21, 2021 @01:06PM (#26531911)
There are two ways of abstracting out an external api, one is via an interface with a set of methods this is well understood but there is another option as well that is to focus on the core data types your application needs. Create those types and then revolve your whole application logic around them.

Finally create decoders that instantiate those types from different sources.

Re:CUPID – the back story by stepbeek on Sunday March 21, 2021 @01:27PM (#26532100)
This sounds interesting. Can you link to any examples?
Re:CUPID – the back story by aszen on Sunday March 21, 2021 @01:46PM (#26532286)
Go through

https://fsharpforfunandprofit.com/fppatterns/

And

https://fsharpforfunandprofit.com/posts/13-ways-of-looking-a...

I'm still learning on how to apply these ideas in my own work (which doesn't use f sharp or functional programming) but it's a rich source of ideas

Re:CUPID – the back story by bandyaboot on Sunday March 21, 2021 @12:53PM (#26531804)
That was one of the eyebrow-raising things the author touched on, “automated testing theatre”. Really? Unit testing is pointless...is that now a thing? Is there another industry that is constantly changing its mind or debating every aspect of itself quite like the software development industry?
Re:CUPID – the back story by jdlshore on Sunday March 21, 2021 @01:03PM (#26531888)
The author is the creator of BDD. I suspect you’re misinterpreting his words.

You can complain about “automate testing theatre” without being against automated testing.

Re:CUPID – the back story by bandyaboot on Sunday March 21, 2021 @01:19PM (#26532031)
Thanks. I’ll read some of his stuff on the subject.
Re:CUPID – the back story by Xunjin on Sunday March 21, 2021 @12:47PM (#26531749)
I wish to know this as well. And what about functional programming languages?!
Re:CUPID – the back story by garethrowlands on Sunday March 21, 2021 @02:15PM (#26532529)
> Honest question: if you don't do dependency inversion, or if you don't depend on interfaces/abstractions that can't be mocked - how do you unit test your code?

The author isn't actually demanding you change your tests (unless they are unnecessarily complicated).

> Unit testing is the only reason pretty much all of my code depends on interfaces. Some people seem to consider this a bad thing/over-engineering, but it's how I've seen it done in every place I've worked at.

Unit tests require 'seams' to divide the tested code into units but those seams don't have to be interfaces. Some languages don't have interfaces at all - Ruby, say - and they unit test just fine. The pattern of every Foo having a corresponding IFoo is a bad thing - you should replace IFoo with smaller role interfaces. Having an IFoo if it isn't necessary to test Foo and there's only one implementation is indeed over engineering.

> I wish to know this as well. And what about functional programming languages?!

Unit testing's much the same in functional languages, though it tends to be easier because pure functions are easier to test. But how code gets its dependencies tends to differ a bit. It may help to think of dependency injection as parameterisation. Or, perhaps, parameterisation where you don't use the parameter immediately.

Suppose I want to test a function foo that depends on another function bar:

    fun foo() {
        return bar()+1
    }
Well with objects and interfaces, I can do this:

    interface IBar {
        bar()
    }

    class Bar(): IBar {
        fun bar() { return 2 }
    }

    class Foo(val bar: IBar) {
        fun foo() {
            return bar()+1
        }
    }
And then I can make a test double for Bar in my test, and when I invoke foo, then it will call the bar of my test double.

Now, of course, in this example, I could test with the real Bar and I don't need a test double. That's partly because the example is simple but also because foo and bar are pure functions. But let's ignore that and see how we can do the same in a functional language.

    fun foo(n: Int) {
        return bar()+n
    }
The question is, how can we control the value of bar in our tests?

The simplest answer is that, since bar is varying (between test and prod), then bar is a parameter:

    fun foo(bar: ()->Int, n: Int) {
        return bar()+n
    }
Now the reason we don't do this in OOP languages is that we won't have bar at the call site. That is the production code calls foo like this:

    foo(n)
and not like this:

    foo(myTestBar, n)
That's why our test code looks like this:

    val myFoo = Foo(myTestBar)
    assert(myFoo.foo(3)).isEqualTo(something)
though, if foo takes its dependency as a parameter, the test could just look like this:

    assert(foo(myTestBar, 3)).isEqualTo(something)
But the prod code won't look like that, it only passes n. So we need to pass bar before that. That's why we pass bar to the constructor in the OOP version.

OOP languages generally expect you to pass all the parameters to a function when you call it but some functional languages don't require this. That is, you can pass bar on its own - a mechanism known as 'partial application'.

You'd use it like this:

    // Do this when 'wiring up' the application
    val myFoo = foo(myTestBar)

    // now I only need to pass n
    myFoo(n)
If your language doesn't provide partial application, then you can do the same with a function that captures the dependency:

    // Do this when 'wiring up' the application
    val myFoo = {n-> foo(myTestBar, n)}
    
    // now I only need to pass n
    myFoo(n)
Now, notice that, since bar is just a function (of type ()->Int), I didn't need to create a separate interface IBar. For this purpose, functions "just work". They are equivalent to interfaces containing a single function (Java's SAM recognises this, if that's familiar to you).

Notice also that an interface of a single function is as small as it can be. Often, we'll see a class Bar with, say, ten methods, and a corresponding IBar also with ten methods.

SOLID's Interface Segregation Principle says to prefer small 'role' interfaces over obese interfaces such as the ten-methods IBar. And, of course, if you're using functions, that happens naturally.

Hope this helps.

Re:CUPID – the back story by Xunjin on Sunday March 21, 2021 @04:33PM (#26533718)
It does thank you, learnt a bit about partial application when dabbled with Haskell, tho never never really got close to Monad stuff (io I guess?!). Now I'm trying to learn more about Rust also I identified references about "Working effectively with legacy code".

My point being, do you have more references for studying about tests?! As a NodeJs and Python developer today (with almost no tests) it's really hard work with these legacy code :|

Re:CUPID – the back story by garethrowlands on Monday March 22, 2021 @05:27AM (#26538502)
Honestly, I think "Working Effectively with Legacy Code" is the main work in this field.

If you can pair with someone who habitually works TDD, do so. Sounds like getting experience in a team with effective modern practices (see "Accelerate" by Forsgren et al) could change your life for the better.

Re:CUPID – the back story by h4kor on Sunday March 21, 2021 @12:49PM (#26531771)
This was one of the most important lessons in my career.

If you want to test your code thoroughly you needs these techniques.

Re:CUPID – the back story by tonyedgecombe on Sunday March 21, 2021 @01:10PM (#26531946)
Whereas one of the most important lessons of my career is that you don’t have to unit test everything.
Re:CUPID – the back story by herval on Sunday March 21, 2021 @12:55PM (#26531817)
The natural segue for this sort of contrarian “I don’t need your patterns” stuff is logically “I don’t need unit tests either”, soooo pretty sure the answer there is “you don’t need tests”
Re:CUPID – the back story by garethrowlands on Sunday March 21, 2021 @01:12PM (#26531974)
Well, he invented BDD, which very much builds on TDD, so, no, that's not the direction he's going.
Re:CUPID – the back story by herval on Sunday March 21, 2021 @01:39PM (#26532213)
Wow that makes it even worse. I had no idea, apologies.