2

Imagine a system for loan applications where loans can be in one of three states: PENDING, APPROVED, REJECTED.

Over time, people create code, UIs, internal reports, etc. that assume there are only 3 states. It's unlikely a single person knows all of the places where this assumption is made.

An idea I had is to create cheap "reminder" unit tests like this that fail whenever a state is added/removed, and let the developer know the potential impact of their change:

assertEquals(["PENDING", "APPROVED", "REJECTED"], LOAN_STATES, "Update finance's XYZ report to handle this new state")

assertEquals(["PENDING", "APPROVED", "REJECTED"], LOAN_STATES, "Microservice XYZ assumes there's only three states, revisit code in file ...")

assertEquals(["PENDING", "APPROVED", "REJECTED"], LOAN_STATES, "Let external API integration partner know about the new state")

How can this be used in a meaningful way to prevent high-impact structural changes from occurring? Are there any related concepts for minimizing or at least detecting the impact of changing business logic?

8
  • 1
    Seems like a lot of hand-holding. The architect/developer of such a system should already know enough about the system to know what would happen if a state is added, and had better know enough about the software requirements to accommodate such an addition. Mar 1, 2022 at 14:56
  • 3
    Come on, downvoters, this question is better as it may look at a first glance. Even if some of you don't like it, in lots of real world systems such constraints exist, and it is sometimes a good idea to implement some automated mechanics to not forget about it.
    – Doc Brown
    Mar 1, 2022 at 16:12
  • 2
    @RobertHarvey: we always tell people "hey, make knowledge more explicit by implementing things into tests (hence less documentation is needed)", and now you say "na, implicit knowledge should be enough"?
    – Doc Brown
    Mar 1, 2022 at 16:19
  • 1
    @DocBrown: Not exactly what I would say. If I had one maxim, it would be "write code that can be easily read and understood by other developers. (hence, less documentation is needed). " Tests are nice, but a determined developer can always circumvent any rules (or tests) you have put into place. Hire developers that play well in the sandbox with others instead of relying on "police code." Mar 1, 2022 at 16:21
  • 2
    Anyway, adding a state to LOAN_STATES implies a major system change (not a bolted-on convenience for some rogue programmer), and such a change would easily be detected in a code review. Mar 1, 2022 at 16:26

4 Answers 4

2

Yes, this can be done easily. As it happens, I wrote a unit test like this just the other day for a similar reason.

In our company's home-grown CRM system, an invoice can be in multiple states, and we need to determine for any given state whether the invoice should be regarded as "paid" or "unpaid". For example:

  • New invoice: unpaid
  • Paid invoice: paid, obviously
  • Canceled invoice: unpaid (because it's no longer due)
  • etc.

The unit test (C#) gets all the current values of the enum in a list. The test also has a dictionary hard-coded with each enum value by name, and whether that state should be regarded as paid or unpaid.

Our CRM has a function that indicates whether an invoice is paid or unpaid. In the unit test, I call that function on each enum value in the list, and verify that its state is correct against the dictionary of expected results.

Now if someone adds a new value to the enum, then the test will fail, because that value won't be in the dictionary; there'll be something like a KeyNotFoundException. There are comments on the test explaining what it means if it fails: The paid/unpaid function may need to be modified, and the dictionary in the test will need to be modified to add that new enum and its paid/unpaid expectation.

6
  • Now if someone adds a new value to the enum, then the test will fail... -- No it won't. Your unit tests address existing states, not new ones. The unit test would only fail if you removed an existing state. I suspect that would cause a compile error anyway. Mar 1, 2022 at 16:50
  • 2
    @RobertHarvey Read again. I call the function on each enum value in the list, not the dictionary. The list contains all the enum values, whether or not they're in the dictionary.
    – Kyralessa
    Mar 1, 2022 at 16:51
  • @RobertHarvey: it should fail because there is no mapping from the new enum value to the paid/unpaid state. Mar 1, 2022 at 16:51
  • Could you not write the paid/unpaid function in such a way that the C# compiler would error on it there was an enum case that it didn't know how to handle?
    – bdsl
    Mar 3, 2022 at 7:45
  • 1
    @bdsl I could, I suppose, but I'm trying to change the system as unobtrusively as possible, because all the developers who originally designed it are long gone, and I'm very new to it. For the most part I'm trying to write what Michael Feathers calls characterization tests, where the tests simply document what the system currently does, assuming that that's correct where there's no reason to believe otherwise. This was an exception where the behavior of the paid/unpaid method was known to be wrong for a particular case.
    – Kyralessa
    Mar 3, 2022 at 8:21
2

I doubt there is a special name for this, however, that does not mean writing such tests is a bad idea.

In a sufficiently large system, there are always certain kind of changes for which the consequences cannot easily be anticipated any more, at least not for anyone in the team who does not carry the x millions lines of code of the system completely in their head. When a system grows beyond a certain size, different subsystem will have non-trivial dependencies with implicit non-local connections.

If your team is in that situation, it is perfectly reasonable to validate certain design decisions by tests. Your enum example is just one possibility. I have implemented tests like that for other non-obvious design decisions or non-local dependencies occasionally into some systems I had to work with. More than once this prevented me and the team to make changes to the code base which would have introduced nasty, hard-to-spot bugs.

Of course, you have to decide where the cost of implementing such tests is really worth the hassle, and where not, so writing tests does not become excessive. One approach here is to implement such "design decision test" whenever you find a case where a bug was introduced into a system because of missing knowledge about non-local dependencies. If you are lucky, this happens during a code review, or by a bug report of a tester. If you are unlucky, this haunts back on you by a bug report of a user. But in every case, it is definitely a good idea to implement a test case which prevents your team to make the same error twice.

1

I would not know how to write tests for this. I would try to address the issue by code design.

In a monolithic system, you could avoid this situation by just providing one enum as "the truth". In a system of systems (and that includes a simple thing like a frontend application with a backend application) this is quite hard because each system needs to know which states exists if it wants to use them. And if for example the backend added a new state, the frontend will only get this information at runtime, when it interacts with the backend.

Therefore, for systems of systems, it is not even possible to write Unit Tests for it. Only if your "unit" includes frontend AND backend code, which is quite too big for a unit test.

We can try it with integration tests, but normally a new state comes into play for new use cases. And how should we write tests for use cases we do not know yet?

Personally, I prefer the "fail fast" approach and would check the incoming data from another system if it suits my expectations. As a result, if there is a new state, the application will explode instantly, and the error/stack trace shows me exactly that the reason is the data from the other system.

In the monolith system each component that uses the central enum could write a test which checks that the enum only includes those values, the component supports.

But that may result in a central enum and lots of (unit test) places which duplicate the enum values. May be still a nice reminder. But personally, I would always check where an enum that I change is used and check all occurrences how my change impacts those places.

1
  • 1
    Suggestion: put your posts through a spell checker before saving them. This only began receiving upvotes after I spell-checked it for you. Mar 1, 2022 at 16:52
-1

In Swift you can mark an enum as @frozen. It’s not impossible to change such an enum, but it will be real work for you, and you better have a very, very good reason for that change. Because whoever has to review it will hate you.

2
  • 2
    If I understand the swift docs, it's only difficult to add a case to a frozen enum if there is code compiled with it that relies on it being frozen - e.g. switches on it and doesn't deal with unknown values.
    – bdsl
    Mar 2, 2022 at 20:15
  • 1
    That’s why you use a switch and not an if - else if - else if … else chain. So you are forced to change the code.
    – gnasher729
    Mar 2, 2022 at 21:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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