Quantcast
Channel: User Filip Milovanović - Software Engineering Stack Exchange
Viewing all articles
Browse latest Browse all 173

Answer by Filip Milovanović for In unit testing: How to abstract a dependency of subject under test?

$
0
0

Mark Seemann makes a good point, but it's important to not take these things as a universal law, and understand them with more nuance. Your GetMenuList method might be too simple for this to apply, but if not, the kind of abstraction the blog is talking about would be based more on the part you omitted (..........), than on the GoogleSheetsService itself. As in, it would be an interface with methods that express the needs (or major steps) of the GetMenuList function, rather than being a generic interface for accessing/modifying sheet data.

I'm not necessarily recommending the following because I don't know exactly what GetMenuList is doing, but for illustrative purposes, suppose the return value was not a simple list, and that the menu logic was a bit more involved (so that there are tangible steps other than just "get the data"), and also that you have to support different versions of the sheet, that differ in how the data is stored. Then, you wouldn't want to hard-code this logic within the function itself - instead, you'd come up with a more abstract interface with methods like (and I'm just making this up)

GetStandardMenuItems(); GetCustomerSpecificItems(customerId); GetMenuLayout();// etc. 

- or something along those lines. GetMenuList would use these internally to express its logic.

In other words, it's a "role interface" consisting of higher-level operations that can be considered common across all the different implementations (what they are exactly will depend on the details of the domain). This interface is an abstraction in the sense that something like GetMenuLayout(...) is more directly expressing what you're trying to achieve (within the context of the caller), whereas GetDataFromSheet(...) is more about the details of accessing generic data (that you ultimately use to achieve it), so this higher level interface is "abstracting away" the specifics of the data-access service. This is, really, dependency inversion applied (it's this sort of abstraction that actually makes it work).

You'd write the logic (and tests) in terms of those higher-level methods, and you'd then delegate reading the data to different implementations of that interface, each of witch encapsulates the knowledge of which cells to access for each particular version. The mocks used within the tests for GetMenuList wouldn't need to emulate the details of an actual sheet, they'd just set up the functions to return the "canned" results needed for a particular test (no more, no less). The kinds of tests you'd write would not be about reading the data, but about how the received information about menu items, customer-specific menu items, and the menu layout is being combined together (or whatever this imaginary version of GetMenuList is supposed to be achieving - you'd test the rules associated with that process).

You'd test the data-accessing dependencies (interface implementations) separately, for things like: are the right cells being read when a method is called (maybe by checking the parameters passed to the underlying service/library that ultimately reads the data). So these tests aren't a repeat of the previous ones, but cover a different set of behaviors. You wouldn't necessarily need to extract a "header interface" out of GoogleSheetsService, but you might need to adjust the design of that general part of the application (by modifying the APIs of your service classes, and/or introducing helper classes) to make this kind of testing easier - if the team feels doing so is worth it in order to get more robust tests. If not, maybe you just use premade test sheets and call it a day (you may re-evaluate that decision at some later point in time, if the need arises).

These unit tests check if the individual components are doing what they are supposed to be doing, and it's important to be clear about what the responsibilities of each component are, so that you know precisely what to test for, and also limit the scope of the tests. Integration tests would give you further confidence that they also work together.

Sometimes, design goals are in conflict with the typical way of using the library, or there are performance considerations, or the added complexity doesn't pay off - so there are tradeoffs involved (but hopefully you can limit the impact of these to the periphery of the application).

That said, in your case, maybe the intermediary level of abstraction of the kind described above is not needed, and maybe you could shift the perspective to also include the calling code, with GetMenuList now being viewed as a suitable abstraction hiding the details of the sheet-reading service from the client code. Again, I don't know what the calling code looks like, or if there is a need for this, but one thing you could consider instead is extracting the IMenuData interface, with GoogleSheetMenuData being one implementation of it, while tweaking the callers so that they aren't explicitly mentioning implementation-specific aspects like "mastersheetid" (although a string "key" is pretty general, so might not be a problem). This then potentially enables you to have your higher level (calling) code support different sources of data with minimal modification.

If going down that route, don't make it too complicated and don't make too many early assumptions, because coming up with an abstraction that turns out to be unsuitable down the line could potentially make things much worse (this is why we have the "rule of three" - it's supposed to help us get a better idea of the patterns and commonalities before we decide what to do). On the other hand, it's also important not to think of the design decisions made early as set in stone, and react in a timely manner (which is often sooner than you'd think), before the codebase becomes a nightmare to work with.


Viewing all articles
Browse latest Browse all 173

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>