
Warming Up to Unit Testing - yumaikas
https://www.junglecoder.com/blog/warming-up-to-unit-testing
======
aazaa
The article doesn't mention the single most import reason for unit testing: to
enable you to refactor your code.

Without a suite of unit tests, there's no way to know that your refactor
hasn't introduced bugs of some kind. A suite of tests won't necessarily catch
every bug you'll introduce, but it will catch many of them. I've seen this
repeatedly.

I've had many conversations with developers who claim they don't need unit
tests. When I ask "How do you refactor?" the answers is usually some variant
of "I don't."

~~~
notacoward
I keep running into the exact inverse: people who seem to believe they _only_
need unit tests (i.e. no functional/integration tests). It's insane. If I had
to pick one, I'd go with the more "live" multi-component tests and ditch the
unit tests. Unit tests still have value, as a lightweight way to catch dumb
errors in future refactors. That's fantastic. Love it. As soon as they cease
being lightweight (and the norm seems to be more effort spent creating
mocks/stubs than the actual tests) then they fail even in that purpose and
just become another impediment to useful work. I'm all for unit tests as part
of a full testing meal, but a whole plate full of nothing _but_ unit tests
makes me want to throw up.

~~~
kraftman
I think mocks in unit tests are a code smell, that indicates that the business
logic has not been correctly isolated from the rest of the code, or that the
code shouldn't be unit tested at all, and should instead be
component/integration tested.

~~~
closeparen
Most architectural guidance encourages separation of concerns into layers
(handler, controller, gateway, etc). Each layer consumes the next. You will be
hard pressed to _unit_ test one layer without injecting a mock of the layer
below.

When the layer is essentially pro forma and has no logic of its own, this is
excruciating, but the line coverage number rules all.

~~~
kraftman
Yeah, I think this separation of concern misses a step.

Let's say you have controller -> Business logic layer -> Data access layer.

The controller takes in some input, validates it, passes it to the business
logic layer. The business logic layer retrieves some data from the DAL, does
some logic, maybe writes data back to the DAL, and passes the data back up to
the controller. The problem is that the business logic layer is usually doing
two things at the same time, in the same function: the 'plumbing' (calling
other things and passing data round), and the actual business logic.

When you try and define a boundary around the code, it gets messy, because
what you want to test, the business logic, is mixed in with the plumbing so
you have to use either dependency injection or mocking to force a boundary.
This makes your tests more brittle as changes in things you don't want to test
(your dependencies) affect your tests.

If you pull out the business logic into its own modules, and instead have a
function that orchestrates the business logic (i.e. tell the database what
data you want, tell the business logic functions to perform an operation on
the data), you end up with a nice clean boundary around your business logic,
which can be unit tested easily without mocks.

You then test the 'plumbing' part by using a component test that checks that
the inputs and outputs of the component as a whole (business logic + plumbing
+ data access) are correct.

~~~
closeparen
I think what you’re describing is what we would call a controller, and
management / senior ICs would still demand 90% line coverage by unit tests.

------
barrowclift
Bit of a disappointing read for me. Given the title, I was expecting more
"meat" to the topic, like more details about _why_ and _how_ the author is now
warming up to unit tests, anything to get the article beyond a surface-level
summary of events. Right now, it simply reads "I didn't previously unit test,
then I read some books, and now I do".

Seems to me writing like this is best served with a few book recommendation
tweets and doesn't really make sense in this format.

~~~
yumaikas
(author here)

Why:

Mostly, I think it's down to having a better understanding of where to try to
break dependencies, specifically trying to abstract away from the outside
world (akin to how they did it in the Moonpig billing system).

The how is still in progress. Perhaps the most notable instance I can think of
it is that recently, due to work on breaking dependencies, I was able to stub
out a RabbitMQ connection for an in-memory class, so that I was able to easily
test how two components interacted without having to stand up an external
service.

For me, looking back, I think it was the Moonpig article that's had the most
influence on how I think about things.

Do you have any other questions?

------
kiwicopple
We started using doctests to do our unit testing:
[https://github.com/supabase/doctest-
js/blob/master/README.md](https://github.com/supabase/doctest-
js/blob/master/README.md)

This was a good way to get our coverage up. Something about writing the ‘test’
in the context of the function makes it seem easier. Also it helps that you
get tests + docs + intellisense all in one fell swoop.

------
justanotherc
I admit I still haven't really latched onto unit testing. I gave it an honest
effort about a year ago, but the amount of time I was spending to mock out the
inputs and then actually writing the test was absurd and I didn't feel
provided enough benefits for the time out took me away from "productive"
coding.

Maybe I'm doing it wrong, who knows...

~~~
notacoward
Maybe, but there are a couple of more likely explanations.

(1) You're working on a system that is poorly structured for testing.

(2) You're trying to make a unit test do an integration test's job.

Both are incredibly common. In particular, if you find that you're spending a
lot of time creating mocks/stubs to support unit tests you'd probably be
better off finding a way to inject errors in an integration test. Error
injection can be done at any level, works even when the code structure makes
mocking difficult, and exercises code paths that cross multiple components in
their "final" close-to-production form. IMX most of the people who go crazy
about unit tests just haven't thought deeply enough about the problem to
realize that error injection is strictly better (and a lot easier) most of the
time.

~~~
justanotherc
#1 is very likely. However if it is extremely common to do unit tests "wrong",
then that would mean I would need to find a testing guru to learn from in
order to do it "right", and do a great deal of study to learn myself. The
possibility of that happening is very low.

Does this not give credence to the notion that there may in fact be something
wrong with unit testing in general? Is it not just compounding the problem?

I'd rather spend my limited cognitive currency and time in the system itself,
not in mastering the art of unit tests... Unit testing should be a tool not an
art. If a tool doesn't simplify my life, it has limited utility.

~~~
notacoward
I don't exactly disagree with you. I've been on many projects where unit tests
failed to justify the time spent writing them (including mocks) and dealing
with spurious failures. I believe they _can_ be beneficial, but only if people
understand their limitations and play to their strengths. I'll gladly use them
to vet code that implements a completely self-contained data structure or
algorithm with no dependencies, and I've done so to good effect many times.
Otherwise, I'd rather write a proper stub/simulator that can be used in
integration tests to exercise multiple components at once than waste time with
single-purpose mocks.

> I'd rather spend my limited cognitive currency and time in the system
> itself, not in mastering the art of unit tests

That's not quite an either/or. Over an entire career you'll probably work on
many systems each requiring their own specific knowledge. Knowing one system
is great as long as you're working on that system, and mostly useless
thereafter. Knowledge of how to write good tests (unit or otherwise) is much
more transferable. _Every_ team can use somebody with those skills.

------
squirrel
Still my favourite source on unit testing and mocking: [http://www.growing-
object-oriented-software.com/](http://www.growing-object-oriented-
software.com/)

------
crimsonalucard
When you first hear about unit tests it seems like the greatest idea ever.
Automation of a process you already do manually makes that process repeated-
able and quick. When I was a junior programmer I was all over it.

Over the years I have come to reject some things that are seemingly
fundamental to software engineering in the industry. One of them is unit
testing. I don't believe that unit testing is harmful in itself, but excessive
use of it is a sign of weakness in another area.

\- if your code relies on unit testing for correctness than your coding
methodology (or language) is error prone. A good programming methodology where
the domain is well understood usually does not need a single unit test.

\- If your code base utilizes dependency injection to make the code more
"testable" with mocks, you are making the code 10x worse. There are other ways
to make your code more modular without injecting new scope, logic and apis
into an object.

This logic sort of applies integration tests which is basically testing the
same thing as unit tests but touching functions that have IO. However it is a
little different. If your program does Not need unit tests but needs
integration tests it means things like:

\- your type checker does not extend across systems

\- the programming methodologies become more error prone as you move to other
systems and you have no control over it.

\- you lack understanding of the foreign system.

The thing with integration tests is that all of the above is largely
inevitable and integration testing really becomes the only way to correlate
(not verify) the entire system with correctness when you lack the integrated
approach of a single programming language. Therefore because of this I am not
against integration testing.

I recently green fielded a small micro-service at my company and I did not
write a single unit test. It had one logic error and that was largely due to a
flaw in the type checker. I can imagine many engineers being aghast at the
whole concept.

So essentially I've had the opposite conclusion. Unit testing to me is a
symptom of bad engineering practices. If you rely on it, something is wrong.

~~~
war1025
> If your code base utilizes dependency injection to make the code more
> "testable" with mocks, you are making the code 10x worse.

I don't know that I'd word it exactly like that, but one thing I've found over
the years is any tests we have that rely on mocks basically just don't test
anything at all.

If your whole test is "This gets called, then that, then that", you aren't
actually writing a test. You're repeating what the code already says.

~~~
crimsonalucard
Well, "something" is being tested as your code is verifying that an input
produces an output.

But your philosophy is correct and we are in agreement on that.

The thing I was referring to is the abstraction of dependency injection
itself. It leads to code that is over complicated as the abstraction itself is
not only pointless but adds unnecessary layers of complexity.

There are ways to target the philosophy of what dependency injection for unit
testing is trying to accomplish without introducing the side effect of
unnecessary complexity that comes with dependency injection. It's as simple as
just separating IO functions from core logical functions. Don't bundle these
two things together into an object or a single function, that's it. (Also
don't use objects period, but that's another topic.)

~~~
l_t
I like dependency injection even when I'm not writing tests (which I usually
skip for side-projects.)

I find it natural and convenient to think about "What dependencies on other
parts of the system does this code have?" Expressing those dependencies
explicitly feels like it reduces complexity, not adds it.

But, I'm not talking about pulling in a big fancy DI framework, just making
dependencies explicit in your function/class parameters.

I will say that DI is sometimes used as a tool in overly-abstracted systems.
An example that comes to mind is the ASP.NET MVC framework -- with DI and
inheritance, literally _any behavior_ can be overridden in fairly opaque ways.
Trying to suss out the concrete behavior details is like swimming in
quicksand. (Or it used to be that way, haven't touched ASP.NET in a while.)

As an aside -- I'm curious about your programming language of choice. I think
DI is a lot more useful in some PLs than others. For example, I find JS code
often uses imports to create complex graphs of implicit dependencies, and DI
can help tame that complexity. But for other PLs like Python or Clojure, I
basically don't use DI at all.

~~~
bernawil
Javascript doesn't "need" dependency injection because modules are objects,
and you can mock those directly by replacing what they reference to. Very
similar story in python. You could say people do DI in these dynamic languages
without calling it DI.

Now Java and C# are different because they are compiled. You need a DI tool to
do dynamic dispatch if you want to mock.

~~~
crimsonalucard
It's all aspects of the same thing.

You can't inject a nuclear reactor into a burrito in any language untyped or
not because it will lead to an error.

The only difference is python/js the error will happen at runtime while in
C$/java the error happens at compile time.

The main difference is the type. In typed languages you need describe am
interface or a class of types if you want to do mocking or dynamic dispatch
while in dynamic languages you don't need to explicitly define this as a type,
the definition exists in how you use the object that is passed in.

Either way DI, however universal, in any language is bad practice.

