DEV Community

loading...

Writing 'Testable' Code Feels Wrong

jonrandy profile image Jon Randy ・1 min read

So, as weird as this may seem, I'm currently in the process of adding tests to a project I'm building at work. Why weird? Because I've never done this before in 25 years as a professional developer.

Some of the code was pretty straightforward to write tests for, but as I progressed it became obvious that refactoring would be needed in order to be able to write tests in some areas. Before this refactoring, the code was succinct, easy to read and had (to my mind) just the right amount of abstraction. The changes necessary to make it 'testable' however, seem to make it overly complex and less efficient just for this 'benefit' of having tests. It just feels... wrong.

Surely the primary function of the code should be that it does the job it's designed to do - ideally as efficiently and (from the developer's point of view) understandably as possible. Going through the contortions and extra layers of abstraction necessary to make it testable seems to fly in the face of these goals.

Does anyone else feel this way? Is this an issue with all languages? Is there a better way of doing things?

Discuss.

Discussion

pic
Editor guide
Collapse
bitschupser profile image
Alex Rampp

Hi Jon,

thank you for sharing this thought. Personally, I'm a big advocate of developing test first since it helps me getting clear about the expectations I have to the unit I'm working on. As I mentioned the term 'unit', unit-testing denotes the activity on verifying that this unit does the right thing.

In my opinion, one very interesting question on unit testing is: what is 'a unit'? Is it a function, a class, a module (whatever that is in you architecture and/or technology), or sometimes a whole (micro-)service? I think there is no general answer - it highly depends on the technology, on the architecture and on the environment you're working in.

When it feels wrong adding additional abstractions just for the sake of test-ability, then perhaps the 'unit' was chosen to be too fine grained. I had an interesting discussion on GitHub a few years ago where I refactored a parser module to make it more testable for the price of making it more complicated. At the end, the original author convinced my seeing the parser as a black box and just test that a given input produces the correct data structure. In this example, I had a wrong definition of 'the unit'.

But sometimes there are also cases where test lead to a missing abstraction. Dependencies to external systems, legacy components or network calls are good candidates for this. While coding, it's easy to just open a network socket and write some data to it. But it's a good idea to abstract these details in some small, low-level components. Sometimes tests lead to such refactorings, since code depending on a network socket is very hard to test. This is the core idea of Test Driven Design.

Collapse
alekseiberezkin profile image
Aleksei Berezkin

Not all of the code needs to be unit-tested IMO. Good test is when the code is complex but the test is easy. Perfect candidates — algorithms, utils, libs etc. Bad candidates: getters and setters, DTO, and boilerplate business logic.

Automated integration tests are a completely different story, but there is also a room for frustration. For example, Selenium tests usually locate controls by tags or classes; and when a UI is changed, tests may break. However not having tests is also dangerous.

So, it seems there can't be strict rule unfortunately. There must be, well, ”reasonable” amount of tests, and the measure of “reason” is very vague.

Collapse
mxldevs profile image
MxL Devs

Usually I just treat it as a black box. As long as it does what it's supposed to do, and not do what it's not supposed to do, should be fine?

If the purpose of testing is to make sure that any new changes or new integrations don't break anything by running a set of tests to make sure everything is still good, a black box test can probably serve that purpose.

Collapse
rbseaver profile image
Rob Seaver

At first, it did feel awkward. I had been programming for fifteen years and had never written a test. When I was introduced to testing and -- more specifically -- TDD, it was jarring. Now, five years later, I've found that testing does help me think better about design and SOLID principles and if I don't have tests I feel like I'm driving without a seatbelt. It took me about a year to get comfortable with TDD, but after repetition and practice, it feels much more natural now. I empathize with where you're at right now, though. I think over time you'll come to appreciate it, and I wish you good luck on your journey!

*Edit: I just realized that this post is several months old. Man, do I hate being late to the party!

Collapse
pentacular profile image
pentacular

I think that to have any meaningful kind of discussion you're going to need to talk about what you mean by "testable", since that's what's driving the changes that you find awkward.

Collapse
jonrandy profile image
Jon Randy Author

Testable using automated test frameworks

Collapse
pentacular profile image
pentacular

That's not very meaningful. :)

A test is just a small application that does something and sees if it got what it expected.

Parts of your code that have no API will be hard to test -- you'll need to simulate a human user.
(But is this useful to you? Hard to know, since you won't talk about your testing requirements)

Parts of your code that have an API will be straight-forward to test -- you can just call them.

But sometimes code operates in a larger environment.
So testing writing to a database will require setting up a database to write to.

The usual approach here is to set up a virtual database of some kind -- perhaps a mock, fake, or light weight implementation.

If your API doesn't allow the caller to supply the databases they want to operate on, then you'll find testing those difficult.

(And for database, substitute any other kind of significant external resource).

And that's pretty much all there is to it.

If your code has APIs, and a way to supply external dependencies, it should be straight-forward to test.

But, again, you need to think about what kinds of tests are actually useful for your use-case.
Unit tests? Regression tests? Integration tests? End-to-end tests? QA tests? Monitoring?

Tests are a cost, so you should concentrate on tests with high utility, and minimize tests with low utility.

So you must actually think about what your testing requirements are, and what it will mean for your code to be testable.