5

I'm trying out unit testing to see if it works for me, but im having trouble with these little functions.

public void OpenNewMenuItem(MenuItemID ID, ITab DataContext = null){
    var menuItem = FindMenuItem(ID);
    menuItem.userControl = menuItem.initUC();

    if (DataContext != null)
        menuItem.userControl.DataContext = DataContext;
    else 
        menuItem.userControl.DataContext = menuItem.SetStandardVM();

    LocalTabs.Add(menuItem);
    GlobalTabs.Add(menuItem);
    SelectedTab = menuItem;
}

I completely recognize, that just trying to write tests for something, makes the code way better. This was a small part of a bigger, harder to understand function before. When I tried to write a test for it I realized how awkward it is to write client code and split it up. Now the calling code looks like this

if (IsMenuItemOpen(ID))
     OpenExistingMenuItem(ID);
else
     OpenNewMenuItem(ID);

But now I don't see much point in testing this part anymore. What is there to test? Its a bunch of assignments and a simple condition.

My process right now feels very off.

Write Code -> write test, if I feel the method is complex enough -> fail to easily write test code -> improve the code -> now methods don't seem complex enough anymore to "test".

I feel like a test would only test if I didn't change anything since last time. As soon as I change something about this method, maybe I want to supply a MenuItem directly instead of finding it via ID, it would break the test anyway.

I would hardly come back to this method and "refactor" it in a way that doesn't change any of the outcomes.

Bilesh Ganguly
  • 343
  • 1
  • 3
  • 13
DFENS
  • 375
  • 1
  • 4
  • 4
    Possible duplicate of [Where is the line between unit testing application logic and distrusting language constructs?](https://softwareengineering.stackexchange.com/questions/322909/where-is-the-line-between-unit-testing-application-logic-and-distrusting-languag) – gnat Sep 08 '19 at 12:14
  • Testing everything is usually overkill so you need to prioritize what you test. Simple behaviours that you can verify directly don't benefit as much from testing. But don't underestimate the ability to do really dumb stuff in really obvious places! Earlier this year I had an extremely obvious function that did `return self.k1.foo(x) * self.k1.foo(x)`. Too obvious to need a test. Shortly later I was confused. Every part of the software seemed to work but still produced wrong results, until I realized: one `k1` should have been `k2`. A 5 minute test would have saved me 3 days of debugging. – amon Sep 08 '19 at 13:25

3 Answers3

8

You aren't required to test at the finest possible grain.

Many current discussions of unit-testing are rooted in a tradition (XP) where features are added incrementally. In other words, almost everything starts out as a "simple" function, and many of those simple functions grow in complexity over time.

So it makes sense, from that perspective, that we would want to have tests even for simple functions.

But... we are ideally not testing functions, but testing behaviors. We don't necessarily care how some particular behavior is achieved. As we reduce coupling to the implementation details, those details become easier to change.

Which means that when we take some well tested module, and extract a simple method from it, we don't necessarily need to run off and create a bunch of new tests for it -- the behavior that we care about is already covered by the tests we have for the code that invokes the new function.

In the case you describe, you are actually going the other direction (which is fine); having extracted simple functions everywhere, you are left with code that is trivial. From a design perspective, this is great - trivial code is really easy to reason about.

Hoare wrote code that is "so simple that there are obviously no deficiencies". In a approach, we tend to work toward designs where code that is expensive to test (ex: I/O) is isolated into modules that are too simple to fail, and ensure that the complicated logic exists in components that are easy to test.

That said, choosing between two alternatives based on a boolean condition isn't actually expensive to test, so the resulting design would normally arrive at a point where the logic is being measured by a "unit test".

But the subject of that test would probably have a larger grain than a single if condition.

i can only test if i test the current IMPLEMENTION

Let me try a slightly different spelling: yes, we are absolutely testing an implementation. The execution of the test is going to observe the behavior of the current implementation, and then check if that behavior satisfies the documented constraint.

We're measuring the implementation against a constraint on the behavior.

If I ran the zoo, we might have a test that looks something like:

opening_a_menu_also_selects_that_menu() {
    // Arrange?
    // Act
    OpenMenu(id)
    selected = isSelected(id)
    // Assert
    assert selected
}

The test doesn't care whether OpenMenu is a single monolithic method, or if it delegates the work to smaller parts. It is utterly hands off on "how", so long as how provides the required API and produces a satisfactory result.

My questions is still "whats the point"? If i change one little thing about my implementaiton, like not keeping a list of globalTabs anymore, the test will break.

The point is largely about being able to introduce new behaviors without breaking old ones. Or to improve the design without breaking the behaviors.

In cases where you have write once code -- because once it is done you leave it alone, or because you change it by throwing it away and starting over -- unit tests don't accrue as much value as they would in a growing code base.

VoiceOfUnreason
  • 32,131
  • 2
  • 42
  • 79
  • I read versions of "test behaviour", "test the contract" "test that the unit does what it say it does" from you and in the other thread. But i cant apply it to my scenario. The contract is "to open a tab", which i can only test if i test the current IMPLEMENTION of how a tab looks in code when its visible. So i wouldnt be testing behaviour, i'd be testing the exact current implementation. – DFENS Sep 08 '19 at 13:09
  • Added an edit in an attempt to clarify - let me know if it helps? – VoiceOfUnreason Sep 08 '19 at 14:06
  • that is pretty much how my test looks. Its just that i also assert the 4 other assignments i make. My questions is still "whats the point"? If i change one little thing about my implementaiton, like not keeping a list of globalTabs anymore, the test will break. – DFENS Sep 08 '19 at 14:18
  • @DFENS Hopefully you have other code around that depends on `menuItem` being added to `GlobalTabs` so it is not an implementation detail but a contract that must be honored for that code to work. Not keeping `GlobalList` is not changing a tiny implementation detail but breaking a contract between `OpenNewMenuItem ` and other parts of your code. – Stop harming Monica Sep 08 '19 at 22:47
  • @DFENS test behavior, but don't test incidental behavior. Test behavior you need regardless of implementation. Test behavior the rest of the system needs to be able to assume will happen. That should be what drives you to test. That way the implementation can change while the test and the code the uses that implementation can stay the same. – candied_orange Sep 09 '19 at 21:34
5

The goal of unit test is not to see if it works for you, but to ensure in an automated way that what worked before still works, whatever change you make to your function.

Of course, for simple functions it’s an extra effort. But this is the price for achieving a high reliability: the idea is not to rely on the developer to decide whether a change or not is worth testing, but testing it anyway. And believe me, there are a lot of bugs introduced by changes that where not thought to be worth testing!

Now a provocative thought: what if instead of writing your function and then your test, you’d change your approach and start to write the tests before writing the function?

  • How can you recognize the success of your function?
  • How would would the function react if the menu item was already open? Or when the ID is invalid?

Another reason to keep in mind is that your function is dependend of other elements. What if these elements change, even if your function doesn't?

Doc Brown
  • 199,015
  • 33
  • 367
  • 565
Christophe
  • 74,672
  • 10
  • 115
  • 187
2

I completely recognize, that just trying to write tests for something, makes the code way better.

As much as developers should be urged towards unit testing, your expectation is a bit off. Unit tests do not improve code. They improve maintainability of the code.

if (IsMenuItemOpen(ID))
     OpenExistingMenuItem(ID);
else
     OpenNewMenuItem(ID);

But now i dont see much point in testing this part anymore. What is there to test? Its a bunch of assignments and a simple condition.

What you're doing now is trying to write tests based on the code you have in front of you. What you should be doing is writing tests based on the purpose of the code.

In this case, the purpose is to decide whether a new item should be opened or an existing item should be reused. The test criteria become quite clear:

  • Does the code open a new item under the right conditions?
  • Does the code open an existing item under the right conditions?

Notice how I did not need to know what the actual code is to create these test conditions, I simply needed to know the functionality that the code seeks to provide.

And then you write those tests. At the moment, your feeling that the tests are effectively rewriting the same logic again is correct, but that is not an argument for not writing the tests.

This is why I pointed out that unit tests improve maintainability. Trivial as the code may be today, it may become more complex tomorrow, or the day after. But your tests remain the same. And no matter how often this function is refactored, the tests ensure that the end result will always be correctly achieved even after refactoring the code.

Unit tests do not help you write better code. Unit tests make sure you don't break something when making alterations in the future.


Write Code -> write test, if i feel the method is complex enough -> fail to easily write test code -> improve the code -> now methods dont seem complex enough anymore to "test".

What you're doing here is not the main purpose of unit testing. What you're doing here is refactoring your code to make it test-friendly, which has the added side effect of making things more readable and less entangled.

Having clear and concise code is a bonus on top of having testable code. Don't use it as a reason to not test the code you just made testable. It defeats the purpose.
At extremes, it can even be a slippery slope into wondering why you'd even need to make your code testable if you're not going to be writing tests for it anyway.

Developers thinking "I don't need to write tests for something I already understand" is THE obstacle in getting capable developers to actually write unit tests. Many developers unintentionally succumb to the arrogant notion that if they understand it today, that must mean that everyone will always understand it.

And the counterevidence is abundant: Developers who have to look at their own code after months of doing something else will struggle to get into the flow of things again. Developers who review other developers' code will immediately notice that understanding someone else's code is not as obvious as the author of the code often thinks.

Unit tests lower the threshold on having to understand code written by others (or by you in the distant past) by only alerting you when an actual problem emerges. And when a problem emerges, the unit test immediately tells you which specific part has failed, which makes it a lot easier to immerse yourself in a precise part of the application instead of having to look at everything altogether.


As soon as i change something about this method, maybe i want to supply a MenuItem directly instead of finding it via ID, it would break the test anyway.

Back to programming basics, there are three steps to every algorithm: input, processing and output. Input and output are the behavior to an external consumer (I press the gas pedal => the car moves forward). Processing is the implementation detail (the combustion engine of the car).

Unit tests only care about behavior. Their main purpose is to ensure that the behavior is maintained when the implementation is altered.

So, yes, if you change your input or output variables, your unit tests will have to be adapted. That is something you cannot avoid.
But if you're spending more time changing the behavior of your solution than you are changing its implementation, something is very off about your development methodology. Taking a stab in the dark, I'd guess that your tasks haven't been properly analyzed before starting on them, thus requiring you to remodel everything as you go.

What is there to test? Its a bunch of assignments and a simple condition.

I assume the listed methods (OpenNewMenuItem etc) are private methods.

Unit tests are written from the point of view of the consumer of your class, and they are only interested in the public behavior of your class. Whether your method has submethods (such as OpenNewMenuItem) or not is irrelevant to the unit test.

So to rephrase your statement, you do not unit test methods, you unit test public behavior (which is generally initiated via public methods).


I would hardly come back to this method and "refactor" it in a way that doesnt change any of the outcomes.

You may not see the point of rewriting it today. But you might in the future. Unit tests are not written so that they help you today, they are written so that they help you in the future.

Flater
  • 44,596
  • 8
  • 88
  • 122