This page shows the source for this entry, with WebCore formatting language tags and attributes highlighted.
Title
The goal is to test everything automatically
Description
So it all started with the following line of code in the <c>Startup.cs</c> of a WPF application,
<code>locator.GetInstance<iauthenticationservice>().LogInBasedOnGeneralSettings();</code>
It was to be replaced with these lines of code.
<code>#if DEBUG
locator.GetInstance<iauthenticationservice>().LogInBasedOnGeneralSettings();
#else
locator.GetInstance<loginviewmodel>().Show();
#endif</code>
<h>Reduce startup complexity</h>
Going by the single-responsibility principle, the startup should be responsible for starting the app but not for making decisions.
The new code makes a decision, so it should be encapsulated in a component.
The simplest (though not testable) way of reducing <a source="SonarCube" href="https://www.sonarsource.com/docs/CognitiveComplexity.pdf">cognitive complexity</a> (PDF Download) is to move the logic to a function. E.g.,
<code>private void EnsureLoggedIn(IServiceLocator locator)
{
#if DEBUG
locator.GetInstance<iauthenticationservice>().LogInBasedOnGeneralSettings();
#else
locator.GetInstance<loginviewmodel>().Show();
#endif
}
</code>
Now the calling code is much simpler,
<code> EnsureLoggedIn(locator);</code>
<h>Define a login service</h>
I also like to reduce calls to <c>locator.GetInstance()</c> as much as possible, so I prefer do define something like a <c>LoginService</c> that consumes the <c>IAuthenticationService</c> and the <c>LoginViewModel</c>, so that you have something like this:
<code>class LoginService(
IAuthenticationService authenticationService,
LoginViewModel loginViewModel)
{
public void EnsureLoggedIn()
{
#if DEBUG
authenticationService.LogInBasedOnGeneralSettings();
#else
loginViewModel.Show();
#endif
}
}</code>
Now we can make the method in the startup file use the <c>LoginService</c> instead.
<code>locator.GetInstance<loginservice>().EnsureLoggedIn()</code>
We don't need to pollute the startup with the nuance of which mode we're in.
<h>Making it testable</h>
A colleague responded that,
<bq>But then you have to [...] inject a ViewModel into a Service?</bq>
I'll include the reply to show how the sausage is made: instead of just showing the solution, I often appreciate learning how people think about problems.
So, here was my response,
I'm not trying to be pedantic; it just comes naturally. π
<ul>I was going to write that injecting a <c>ViewModel</c> into a service isn't bad because it's just a view model. But then I noticed that it seems to be communicating with the view in order to show something to the user. π
We're trying to abstract away complexity and to make our logic testable.
We need to call <c>Show()</c> during startup; that's a fact. If we introduce a service, it actually makes that part mockable.
If we wanted to test that the <c>LogInBasedOnGeneratedSettings()</c> is called when expected, we couldn't do that right now, could we?
If we make it a service, then we could think about verifying the logic with a test.
Of course, once we want to build the test, we'd then be confronted with the need to abstract away the compiler-define. Otherwise, we wouldn't be able to test both branches without recompiling. That's a code smell, too.
Which is why I usually end up with some standard settings objects like the following.</ul>
<code>public interface ICompilerSettings
{
public bool IsDebug { get; }
}
public class CompilerSettings : ICompilerSettings
{
public bool IsDebug
{
get
{
#if DEBUG
return true;
#else
return false;
#endif
}
}
}
public interface ILoginServiceSettings
{
public bool ForceLogin { get; }
}
public class LoginServiceSettings(ICompilerSettings compilerSettings) : ILoginServiceSettings
{
public bool ForceLogin => !compilerSettings.IsDebug;
}</code>
You might think that this is over-engineering, overkill, an excess of ceremony introduced by an architect astronaut!
I, on the other hand, think that this is a minimal solution that separates concerns and makes all branches testable.
Once we inject the <c>ILoginServiceSettings</c> into the <c>LoginService</c>, we can easily verify the behavior with tests (using fakes, mocks, etc.).
<code>class LoginService(ILoginServiceSettings settings,
IAuthenticationService authenticationService,
LoginViewModel loginViewModel)
{
public void EnsureLoggedIn()
{
if (!settings.ForceLogin())
{
authenticationService.LogInBasedOnGeneralSettings();
}
else
{
loginViewModel.Show();
}
}</code>
It might look like a lot of ceremony but, without it, how else can you say with confidence that the login is required in some cases but not others? We can even verify that it's not required in <c>DEBUG</c> mode by mocking <c>ICompilerSettings</c>.
Then the only thing we have to verify without automated tests is that the <c>CompilerSettings</c> are implemented as expected, which is very little code to manually check. We don't need to look at the rest. π
<h>We want to test everything</h>
My colleague very politely responded,
<bq>Injecting ViewModels into Services is generally considered bad practice. The rest seems to depend on what you wish to test and don't overengineer it...</bq>
At this point, we took the conversation to meatspace, i.e., I ran over to his desk to tell him that "I always want to <i>test everything</i>." I am willing to concede on time constraints, priority, and planning, but my goal is "test all the code paths" <i>eventually</i>. I'm patient, though, so will accept unwritten tests as technical debt. I will design my could so that it <i>could</i> be tested, though, when we eventually find time to do so. This
We shouldn't just punt on tests because "it looks difficult" or "it's not much logic".
In the first case, the fact that it looks difficult may indicate you're not writing your code in a testable way or may reveal architectural problems. In the second case, those are famous last words. If it's just a little logic, then why wouldn't you just test it instead of investing the time arguing that you don't need a test?
If you have a code base thatβs difficult to test because of some unfortunate architectural decisions, then the thing to do is <i>not to ignore it</i> but to <i>slowly chip away at it.</i>
How else would we get a higher percentage of our code covered by tests? Hint: it's not by continuing to write more code without tests.
<h>Addendum: A note on architectural boundaries</h>
He'd also argued about mixing levels---injecting a <c>ViewModel</c> into a <i>service</i>---but I convinced him that this is already what was happening whether you wrap a service around it or not. The startup is already instantiating and using a view model. Is that somehow better?
I don't think it's a bad thing, as it's just a way of asking the user for input in order to continue starting the application. It's a step in the application startup. If you wrap it in a service, then you can at least test that the code does what you want. <b>This is exactly the kind of thing that everyone is going to forget to test manually.</b>
Should the startup be using the view? Maybe, maybe not. It currently <i>does</i> and it makes for a legible workflow. Being too pedantic about architectural boundaries <i>just for the sake of it</i> is often wasted effort and can often lead to overly complex solutions.
Ordinarily, the problem would be that introducing a dependency across a boundary weakens testability, but that's obviously not the case here. We can just mock away the view and test all of our logic.
I wouldn't worry about this one too much, though it's a good rule of thumb.