2

A little background on the project: we as a company receive a spaghetti source code, and into that we add even more spaghetti code. So with that I want to say that

complete restructuring and refactoring of the code and un-spaghettifing it is not really possible

and we also do not have any say in what the other company deliveries,

we just have to deal with what we have.

It is a C/C++11 code with CMake. I chose Google test framework (but I can still change it).

The structure is something like this

src/ModuleA/
├── CMakeLists.txt
├── include
│   ├── Submodule1
│   │   ├── Submodule1.h
│   │   └── SomethingElse
│   │       └── SomethingElse.h
│   └── ModuleA.h
└── src
    ├── Submodule1
    │   ├── Submodule1.c
    │   └── SomethingElse
    │       └── SomethingElse.c
    └── ModuleA.cpp

Naturally there are tons of modules, submodules, headers and source files
And ofcourse they include each other all over the place and between the big Modules!

Now the question

what steps would you recommend to a noob unit-tester (I'm actually a developer, not tester) to isolate tightly coupled functions to unit test them?

In addition to that - what folder structure would you recommend? And what CMake structure would you recommend to make it easily manageable in the future (reusing mocks/fakes, untouched headers) to avoid manually adding tons of code for each unit over and over.

1
  • 2
    As always, read Working Effectively with Legacy Code. Commented Jun 14 at 14:30

3 Answers 3

4

Although you'll probably find good approaches to working with poorly-designed spaghetti code in the question that Kilian Foth linked to in the comment, since you're specifically interested in testing, there is a very specific solution.

This is the kind of problem that characterization tests were designed to solve. When you can't test your functions and classes independently in well-defined units, you can write higher-level tests that capture the current behavior of the software. This can give you the confidence that you need to refactor the code behind the public interface. You may want to introduce something like a facade or adapter to keep your expected public interface while doing the refactoring and testing against that public interface. As you refactor and introduce more testable units, you can add additional unit and integration tests and phase out the characterization tests.

6
  • Another option is write automated end-to-end tests. Some systems just cannot be tested any other way than through the user interface (provided there is a user interface to test). Commented Jun 14 at 14:41
  • @GregBurghardt I think that characterization tests can be end-to-end tests. The key difference is that characterization tests capture what the system actually does, even if it's wrong. When you are refactoring, you don't want to introduce behavior changes and, in the absence of good requirements or specifications, you may not actually know what the correct behavior is or why.
    – Thomas Owens
    Commented Jun 14 at 14:44
  • Ah, ok. I've always had a problem distinguishing characterization tests from end-to-end tests. The key bit is "how the system actually behaves" rather than "how it should behave." Commented Jun 14 at 14:48
  • @GregBurghardt That is my understanding of them, anyway, from reading some of Michael Feathers' work and people who have interpreted Feathers' work. Unfortunately, I've never read the source material, which is Working Effectively with Legacy Code. Working Effectively with Legacy Code is also 20 years old, so the perspective may have also evolved from what was originally written there.
    – Thomas Owens
    Commented Jun 14 at 14:54
  • We already have high-level test which make sure that the end result does what is expected, but they can take around 24 hours. I'm trying to make unit test to run them a in a few milliseconds (seconds or minutes at worst) on each commit. Commented Jun 17 at 11:26
0

Write unit tests for whatever "Unit" or "Module" there is. If everything is tightly coupled to everything else, then you are kind of forced to treat it all as a single "unit", and write your tests for your whole program.

An important part of automated testing is to clarify and document the current behavior of your program, so you know if refactoring introduce any regressions. Note that automated testing is not the only way to do this, you could also create manual tests, or write comprehensive documentation/specifications. It is also possible the current behavior diverges from the specified behavior, and that would be of particular note.

Keep in mind that the "unit" in unit testing does not necessarily mean a single class, "units" can be of any size. The important thing is that the unit has high cohesion and low coupling. It would be common to have tests at multiple levels of abstraction.

As you start restructuring the code you likely want to continue writing tests as you go, as soon as you manage to chop of some piece of functionality, write unit tests for that part so you know it won't break later on.

A final point is that you want to test the "public behavior" of the unit you are testing, and try to avoid relying on anything implementation specific that might be subject to change. This can make it much more difficult to write acceptance criteria. As an example, think about how you would test a pseudo random number generator, without making any particular assumption about its internal algorithm.

1
  • 1
    I'm afraid that in my case "the unit" is the entire app :D And the public interface is a command protocol - not really API - just send and receive. Commented Jun 17 at 7:22
0

As an addition to Thomas Owens' nice answer, I think there are often opportunites to start with adding unit tests whenever you have a requirement to change something. Let's say you already expect for a new requirement the need to add some more code to an already too-long function in some module.

Before you start refactoring the function, think about which part could be refactored to a smaller module or function, so it would make adding the new requirement afterwards easier. Then copy (!) the related part into a new module and/or function, write several unit tests for the copy and make sure it works as intented - in isolation. Maybe you can already extend this new function or module in a way it will support parts of the new requirements.

After you are pleased with the new function/module's quality and have some confidence in it, you start refactoring the old code, and remove the parts which are now superfluous by calling the new code from the old code. Of course, at that time you should have the characterization tests in place suggested by Thomas.

So in short:

  1. Create new, modular building bricks which are more easily testable, for which you know they will help you to implement a certain requirement.

  2. Write unit tests for the new building bricks.

  3. Simplify the old "spaghetti" code by introducing the new building bricks.

7
  • Yeah, that's exactly what I'm trying to do - I made a change, and now I want to make a unit test to that module... but how? It's pain in the *** to start in this environment. Commented Jun 17 at 11:34
  • @TomášViksPilný: I thought I was as clear about the "how" as one could be without knowing the actual code. See my summarization which I added right now, maybe it clarifies it a bit.
    – Doc Brown
    Commented Jun 17 at 11:55
  • I find it really annoying that this answer mentions "function" level refactoring, when question clearly states problems on intermodule levels. To be able to modify a single function is a blessing, not a given.
    – Basilevs
    Commented Jun 17 at 13:28
  • @Basilevs: oh dear, I was expecting readers of this question to be smart enough to transfer what I had written to their specific situation. Functions are often the smallest sensible items for unit tests, but when the smallest testable unit is a module, class or component, then I guess readers can figure out by themselves what makes most sense for them
    – Doc Brown
    Commented Jun 17 at 13:35
  • The question is very explicit about the whole application being the smallest testable unit. And it is not a problem of "transfering" the whole issue with unit tests is that they require a "unit".
    – Basilevs
    Commented Jun 17 at 15:13

Not the answer you're looking for? Browse other questions tagged or ask your own question.