I want to unit test an extension method for a type WebSiteResource, where this type WebSiteResource
is an implementation of an abstract
type. I need to set or mock a value to a property, but it's not virtual
.
public static string HostName(this WebSiteResource webSite) => webSite.Data.DefaultHostName;
- I can't mock
WebSiteResource
, because it's not an interface - I can't instantiate
WebSiteResource
because propertyData
requires complex setup - I can't create a
MockableWebSiteResource
that inherits fromWebSiteResource
(and doing something likepublic new virtual Data { get; set; }
) becauseData
requires complex setup
How do you design methods to avoid making them non-testable?
Update (after answer):
What I in fact have is a non-trivial method like
public static bool IsValid(this WebSiteResource webSite) {
// and then complex logic for multiple properties etc
}
CodePudding user response:
I want to unit test an extension method...
The string HostName()
method you posted is a trivial function: I don't think there is any value in writing any kind of test code for it.
Automated tests are great, but they introduce their own brittleness and maintenance burden. If it took you 3 minutes to research and write that function, it's now going to take you far more additional minutes to think-up all the different weird and exotic edge-cases where your HostName
function will fail (e.g. website.Data is null
under some obscure network situation or race-condition).
Disregarding this specific function, but in general: regarding how to design testable code which directly consumes objects from opaque external libraries... well, the simple answer is don't: (as in: don't write application code that directly consumes external library objects and directly passes those objects deeper into your own application code).
So instead set-up clear boundaries: software bulkheads between distinct components in your application code and never let a naked external object cross that line. If you need to pass some data from an external library across the line then introduce your own POCO data-transfer-object and copy the values into it, then pass that along.
The alternative path of least resistance only leads to worse outcomes in the long-run: eventually every project in your solution has to reference all the same NuGet packages and assembly references so external library objects can be passed around (such as by passing that WebSiteResource
object all the way directly to your user-interface).
....so you'll run into more and more of the problems like you've illustrated: even though it's your project you no-longer have any real control over the opaque objects you're using: in order to accomplish some future goal you'll likely find yourself fighting those libs and needing to write hackish code to work-around them instead of just directly editing their source and recompiling them, because you don't control them.
Sidebar: Personally, I blame C#'s inflexible type-system...
I note that a large part of this problem is simply because of how C# and the .NET CLR works: that ultimately you're restricted from manipulating that WebSiteResource
type (and its object-instances) by the .NET type-system (...at least in Java everything is virtual
).
Because C#/.NET uses a nominative type-system that's strictly enforced by the runtime, and how it doesn't support structural-typing or true algebraic types it means we can't do things like class HahahImSubclassingYouAnyway extends WebSiteResource
(or even class HahahImSubclassingYouAnyway implements WebSiteResource
in some languages). At least we can use composition (which we should be doing anyway), but again, C#/.NET is still dragging its feet when it comes to making it easy to compose types together, not to mention how it's impossible to maintain reference identity (which is why we're forced to use inheritance-based subclassing in some situations).
Anyway, I'm ranting...
I can't mock
WebSiteResource
, because it's not an interface
- Assuming you're still dead-set on this, then implement your own adapter types.
- Yes, it's exactly as tedious and error-prone as it sounds.
- Yes, this is why it's probably an inappropriate use of your time to work on this.
I can't instantiate
WebSiteResource
because propertyData
requires complex setup
The fact you wrote that makes it sound like you're thinking of writing test-cases that will essentially test the WebSiteResource
object far more than your own code. I know that's not your intention, though.
I can't create a
MockableWebSiteResource
that inherits fromWebSiteResource
(and doing something likepublic new virtual Data { get; set; }
) becauseData
requires complex setup
Whoa, stop, hold it right there...
What you're describing is an abuse of OOP inheritance. Your proposed MockableWebSiteResource
clearly shouldn't have an "is" conceptual relationship with WebSiteResource
, but that's exactly what OOP class inheritance represents (and WebSiteResource
represents a fully-fleshed out service implementation, not an abstract service). This is a sign that something isn't right with your approach. Stop. Think. Reconsider.
(And because Java-style OOP inheritance (which C# shares) is so limiting and inelegant it just ends-up creating problems instead of solving them: it's just bad. I'm sure you've run into situations where you found it impossible to use OOP inheritance for any kind of direct domain modelling).
(With apologies to Smalltalk and Simula fans (we love you), but I'm using the term "OOP" in the Java-family sense: so I am not referring to the broader (or purer) higher-minded concepts of O.G. OOP where message-passing and delegation are used instead of virtual methods and inheritance)
How do you design methods to avoid making them non-testable?
I can't give you anything other than general guidelines that we've all head before:
- Writing "unit testable" code?
- https://softwareengineering.stackexchange.com/questions/288405/is-testable-code-better-code
- Somewhat contrary to my advice to define your own service/adapter types, this recent article from StackOverflow recommends testing with "real" services (of course that would be an integration-test, not a unit test...). But I digress.
- Think critically, of course: https://medium.com/@first.last/unit-testing-the-worst-of-stackoverflow-2fef3bd85ed9