13 ms·
Prefer Fakes over Mocks
- Ronsenshi 6y agoNot particularly convincing. Mocks work well because you can quickly define them in a given unit test and forget about it. I can see fakes being useful in certain cases, but only just in certain cases. Could be my inexperience with C#, but it didn't do justice describing the point in code samples.
- ddek 6y agoI question a lot of advice like this, and agree with you. I’d generally far rather use NSubstitute than have to write fake classes, but I can see the utility in some cases. My honest response is this: Writing tests should be quick and painless. Maintaining fakes is more painful than maintaining substitutes. Substitutes let me interrogate side effects in great detail. I’m not concerned will performance in unit tests. Ergo, substitutes are quicker and less painful, so IMO that’s a win. But for real though, why should anyone care about someone else’s test architecture, of all things. As long as you neither under- or over-testing, why on earth does it matter?
- doubletgl 6y ago> why should anyone care about someone else’s test architecture, of all things. As long as you neither under- or over-testing, why on earth does it matter? If you have to maintain tests written by other devs you might start to care. Devs apply different patterns to writing tests and show different amounts of discipline doing so. Some produce lots of duplication ("it doesn't matter, it's just a unit test"), others prefer a complete setup and tear-down before and after every little assertion. I've seen things..
- andix 6y agoI've seen unit tests that test around bugs. Once you fix the bug, a lot of tests fail. Then you discover there is no easy fix for the test. Either delete them all or replace them with new ones.
- deleted 6y ago[deleted]
- jrockway 6y agoI tried to distill down the basics a few years ago in a 1 page article: https://testing.googleblog.com/2013/06/testing-on-toilet-fake-your-way-to.html https://testing.googleblog.com/2013/06/testing-on-toilet-fak... The example is a little contrived, but the whole article fits on one sheet of paper and does yield a better test than mocks would. Testing against the real system is always better than testing against a fake. That should be your first preference, and if you can modify the system to make testing against it easier, that's even better. Sometimes you can't, and if your interaction is complicated and the parts that you care about are simple, a fake can get you good confidence that your part works. Mocks just feel like writing the code again -- in your code, you write "call a function with an argument, then call another function with an argument" and in the tests you write "pass if a function is called with an argument, and again with an argument". It can be the right thing at the right time, but it's usually writing a test to make some automated linter shut up. And that's a complete waste of time.
- rocqua 6y ago> if you can modify the system to make testing against it easier, that's even better. What do you mean here? Changing your testing system, or changing the real system?
- jrockway 6y agoChanging the real system to add the hooks you need. Complexity often exists around waiting for events to happen (the real system doesn't need notifications, but you don't want your tests to run forever when they're failing), clocks (time.Now() is difficult to test), things like that.
- rocqua 6y agoPersonally I am loath to add extra functionality to the real system just to make testing easier. I am massively in favor of keeping the complexity in the real system down. Adding more hooks, or increasing the interface of the real system, just to make it more testable feels wrong. This is why I'd want test to just have a lot more 'leeway' than normal code. Essentially I want heavy hooking, introspection and reflexivity but only for tests.
- SideburnsOfDoom 6y ago> Mocks work well because you can quickly define them in a given unit test and forget about it. They do, yes. But: * the mock syntax is complex and verbose. e.g. "repo.Setup(x => x.Method(It.IsAny<string>(), It.IsAny<List<int>>())) .ReturnsAsync(() => ...." is not the easiest syntax. The syntax for a fake is just regular old "class FakeFoo: IFoo" with trivial method implementations and no library needed to make it work. * the mock syntax does not scale. Your 5 lines of setup in one test does not help you in a different test class. So it gets repeated. Soon you might find that the Fake is easier to read, easier to maintain, and fewer lines of code than all the mocks. As the article says: "their self-contained nature makes them reusable as well, which lends itself to better long-term maintainability." Not all our code is "do it quickly and forget about it", especially where you see that same thing or minor variations of it done over and over again in related tests. Mocks win here when you do something different, once only. Fakes win in the opposite cases. > I can see fakes being useful in certain cases, This is true, but it's also true that Fakes are _underused_ in general in in the .NET world. People reach for mocking frameworks as if they are the only tool.
- hackerfromthefu 6y agoI completely agree with you
- srtjstjsj 6y agoWith a fake you don't need to rewrite its behavior in every single unit test that friends on it. One fake for each datastore, shared by all tests.
- deleted 6y ago[deleted]
- doubletgl 6y agoHow are fakes, as described in this article, not also implementation-aware testing?
- andix 6y agoBecause they don't just implement a part of the interface (just one store method, or just one of two load methods). They try to create a fully functional fake of the original class. In this case instead of storing the blobs via a REST interface or a database connection, it just keeps it in memory. So it should mostly behave like the real implementation, but is not suitable for production, as it doesn't persist the data. The only things that are missing there are real life latency or maybe exceptions because of connectivity issues.
- hackerfromthefu 6y ago>> .. fully functional fake of the original class. So they are coupled to the implementation of the object in any case!
- silveroriole 6y agoI’d say rather that they’re coupled to the behaviour of the object.
- tyrrrz 6y agoQuoting the article: > It may also appear that the details we have to take into account here are not that different from the implementation-aware assumptions we were making when using mocks, as neither are actually governed by the interface of the component. However, the major distinction is that the coupling we institute here is between the test double and the real implementation of the component, rather than between the test double and the internal specifics of its consumer.
- deleted 6y ago[deleted]
- andix 6y agoFakes are great for objects that store data. Like in this example. If I use Entity Framework Core I also love the in-memory provider (or SQLite in-memory), which gives you a very well faked database context for unit tests. In some cases you can create end to end tests, that run and verify the complete business logic, without hitting the database at all. A mixture between unit and integration tests. https://docs.microsoft.com/en-us/ef/core/miscellaneous/testing/in-memory https://docs.microsoft.com/en-us/ef/core/miscellaneous/testi...
- zabil 6y ago> If I use Entity Framework Core I also love the in-memory provider (or SQLite in-memory), which gives you a very well faked database context for unit tests. This approach easy to setup, however I found that it slows down unit tests especially with data migrations and setup before each test (to discard test data from other tests). I've since limited these to test only migrations and in some well thought out integration testing.
- silveroriole 6y agoThe fatal flaw is that you’re really introducing a new class which may have its own behaviour problems. Now your test is implicitly testing the test class. I also prefer to see explicit mocking than just trust that the fake does what it says it will. How do I know without looking whether the fake store method REALLY stores the doc, or whether someone set it up to just return OK? And how do you test error handling - make another fake for every error scenario?
- andix 6y agoMocks have the same problem. You create something new, that can have uninteneded behavior. Even worse, if you are not perfectly familiar with the mocking framework, you maybe won't even notice it. The good thing about (simple) fakes is, that you can see all the code in a very small class. No complicated library, that does something funky in a special case. Mocking can be very useful though, but it can get too complicated very quickly.
- silveroriole 6y agoMocks are essentially just “calling this function on the mock object during this test returns this result”, though, right? At least, the ones I write are. No state, no logic, no real ‘behaviour’. As soon as you introduce a fake that really has an underlying state and maybe some kind of validation logic (assuming that you want tests to make sure you can’t insert nonsense/retrieve non-existent stuff), you’ve introduced a ton of new and completely meaningless failure points.
- andix 6y agoYou're totally right. But think about a blob storage class that has two methods, ReadOneFile(), ReadMultipleFiles(). You have one test that just mocks ReadOneFile(string fileName) and doesn't mock the ReadMultipleFiles(). You now change the implementation of the System Under Test (SUT) to use once the ReadMultipleFile(string[] fileNames) call instead of three times the ReadOneFile(). Now your test fails, but the implementation of the SUT is perfectly valid. You need to rewrite your test. If you would use a fake, the test would stay green, without changing. The test with the fake is less specific to the implementation, and helps you better with refactoring/changing your code. And additionally in this example ReadOneFile() is called three times, and is expected to return three different results. So even your mock needs some logic to handle that.
- tomgp 6y agoA tangent, and somewhat philosophical: is "The primary purpose of software testing" really "to detect any potential defects in a program before it reaches its intended consumers"? I would argue that whilst that is a commonly held belief, and certainly true for some tests of business logic, the main value and reason for testing is to allow developers to confidently make changes to existing code. In my experience user acceptance testing is a more useful tool for detecting user facing defects as it also tests the developers assumptions about functionality.
- FartyMcFarter 6y agoBut if we didn't care about defects, developers could confidently make changes to existing code without having tests. The end goal is still avoiding defects, helping developers is just the "middle-man".
- nickff 6y agoI agree with this view, and would like to add that I see improving the tests available to the developer as really speeding up the OODA loop of development.
- hinkley 6y agoThe interim goal is objective confidence, not Dunning-Kruger confidence. That’s how you go faster. There’s always someone who’s willing to plow ahead, safeties off, and ignore the warning signs they see and the complaints of their peers. Denial is a hell of a drug (“That bug can’t be in my code, la la la can’t hear you look how fast I’m going!”). Some people need a little back pressure.
- rocqua 6y agoI agree, but the two are very similar if you read 'defects in a program' as bugs. Bugs can be hard to cause, and thus not be noticed by user acceptance testing. So for catching them, tests are more useful.
- deleted 6y ago[deleted]
- scresswell 6y agoI've tried this approach twice with mixed success. In both cases I wanted to stub out the persistence tier of a Node.js application when test driving the API. I verified the real and fake implementations by running the same tests against them. The first application was quite small, and the process worked well, although it did feel somewhat onerous to implement the fake. However, the second application was more complex, calling for some PostgreSQL specific features which were difficult to implement in the fake. In the end I abandoned the approach for the second application, settling on slower, duplication heavy API tests which used the real persistence tier. I'd love to hear how others solve this problem without mocks, which I agree tend towards brittle, tightly coupled tests with a poorer signal to noise ratio.
- chriswarbo 6y agoMy hierarchy goes: None > Real > Fake > Mock The best solution doesn't need anything, e.g. if we have calls to our persistence layer mixed in with calculations, the latter shouldn't be tested with fakes/mocks; instead we should refactor the code so the calculations don't depend on the persistence layer. If the behaviour of some code depends inextricably on some external system, then our tests should use that system. This avoids unnecessary code/work/duplication, allows tests to exercise more paths through our codebase, exposes problems in our understanding of that system, etc. If calling an external system in our tests is dangerous, unacceptably slow, costly (if it's some Web service), etc. then we should make a fake implementation to test against. If our code only uses a small part of some large, complicated dependency, we might want to define an interface for the subset that we need, refactor our code to use that, and only have our fake implement that part. If a fake implementation isn't practical, or would require so much overriding per-test as to be useless on its own, then I might consider using mocks. I would seriously consider whether that code could be refactored to avoid such coupling. I would also never make assertions about irrelevant details, like whether a particular method was called, or what order things are run in.
- csala 6y agoSimply saying prefer "this" over "that" without adding any context to the equation is the best recipe to end up doing the wrong thing in the wrong place. Or maybe you mean that you can make the same decisions when testing a Web based app UI than when testing a cryptographic function in the Linux kernel? IMO, both mocks, fakes and "reals" (to put it in the same language) need to be first class citizens of software testing, each one of them used in the right circumstances. Just like resorting to unit testing only is as much a bad practice as it is to use acceptance testing only. A good testing strategy should be adapted to each software and context, and potentially include all types of tests: unit, end-to-end, integration, numerical, acceptance, etc. and all sorts of resources: mocks, dummy data, substitutes, testing engineers, test databases, real databases, real users, etc.
- gitgud 6y ago> "However, relying on dynamically-generated mocks can be dangerous, as it typically leads to implementation-aware testing." I agree, tests should be simple! Dynamically generating stubs, mocks or even arguments for tests hides complexity which leads to test code that's hard to reason about... and is hard to change/fix
- mathgladiator 6y agoIt seems to me that a Fake is really a Mock, and Mocks became this unholy dynamic framework stuff. I'm not a fan of dynamic mocks where the test code has the mocked implementation because it is super brittle. Instead, a "fake" (i.e. a mock as I understood them) is an alternatively implementation of an interface. There are a lot of really good "fakes" out there. Fake file system which lets you simulate disk failures. Fake time and randomness to introduce determinism. Every API over network can have a fake. These are all good things to fake out as you can simulate the appropriate failures which are almost impossible to do via integration, acceptance, or E2E tests.
- viraptor 6y agoThe problem with those alternative implementations is that they turn your unit tests into expensive and complicated integration tests. In a complex system getting a socket into the state you're interested in can be many times harder than the test itself. "Magic" mocks make it cheap and easy.
- chriswarbo 6y agoThere's nothing preventing a fake implementation of interface Foo from also having extra functionality, like controlling it's initial state. In the case of sockets, we might have a FakeSocket which implements methods Socket::close, Socket::read, etc. but also provides methods like FakeSocket::appendToBuffer, FakeSocket::withState, etc. The boundary between such fakes and mocks is obviously fuzzy: as we override more of a fake's behaviour we approach a mock, as we make a mock more functionally-complete we approach a fake. Mocks (in this sense) are most useful when we want a lot of control over a very simple interaction; e.g. having Socket::read return a particular error state. We don't need a whole fake implementation for that, and indeed it's probably easier to just specify a mock with only those parts (and nothing else). In my experience such situations are rare, since they tend to result from over-complication in the system under test, which makes it hard to test. Mocks are a useful crutch in those situations, but it's often better to refactor the code such that mocks aren't needed. For example, whenever we create a mock that returns some canned response, that's a hint that our 'service object' is probably just a (possibly lazy) wrapper around that response. Any code that processes such responses can often be refactored into an ordinary function/method which accepts such response data as an argument. Sending test data into such functions is trivial (just call the function on the test data), and we can delete whatever wrapping/unwrapping boilerplate was previously littering the code. When our mocks are more complicated that just spitting out canned responses (e.g. there's some temporal dependency between method calls, and correlations between their responses), those are exactly the things that fake implementations are for.
- pjmlp 6y agoFakes can also be mocks actually, https://docs.microsoft.com/en-us/visualstudio/test/isolating-code-under-test-with-microsoft-fakes?view=vs-2019 https://docs.microsoft.com/en-us/visualstudio/test/isolating... Which I initially thought was the topic being discussed.
- mcqueenjordan 6y agoEven more important, IMO, regardless of whether you're mocking or faking, is to not mock or fake objects that you own. Only mock/fake truly external dependencies. The nice property you get with this is true tests of integration and interoperability between your modules, that are actually calling into each other. Only at the leaf nodes is anything mocked or faked, and that's when it leaves your ownership.
- viraptor 6y agoYou're choosing some place in between the integration and unit testing where your test will live. But this choice will not make sense for everyone. If your project includes its own persistence functions, you'll likely want a test to get a "CouldntWriteException" immediately from a mock rather than go through all the layers (that you own) to get an IO error (you don't own the fs dependency) and all layers back. It's a tiny difference for a single test, but once you get to hundreds, it will be a difference between devs getting immediate feedback and "going for coffee, my code is testing" which really impacts productivity.
- ratww 6y agoYes! Mocking internal dependencies also has the side effect of ossifying the internal interface between the two objects, which is never as well-designed as the external ones IME. Also, in dynamic languages, you might even have tests that don’t break when you change (or sometimes even delete) the dependency because the mock was correct. That leads to a false sense of confidence.
- paranoidrobot 6y agoPerhaps I misunderstand you. Say I'm writing tests for lets say ShoppingCart. ShoppingCart depends on an implementation of ICartDiscountCalculator (CartDiscountCalculator) CartDiscountCalculator depends on a number of implementations of ICartDiscountStrategy (ChristmasCartDiscountStrategy, VIPCustomerDiscountStrategy, BulkPurchaseDiscountStrategy, etc) Each of those, in turn has their own dependencies, perhaps webservice calls, db calls, accessing caches, etc. The way I would test ShoppingCart would be to mock out ICartDiscountCalculator, and provide known responses. Are you suggesting instead that you build up the dependency graph - i.e the actual implementation of the discount calculator, the strategies, etc - and only mock when it goes to external code (eg the DB calls)? This would seem to make testing very very difficult - since each test needs to know implementation details of everything it depends on.
- 3pt14159 6y agoI'm a little surprised this article and none of the contents here at the time of this writing mentioned inheritance. The core reason I find OOP to be superior for most classes of business application development in general—and web development in particular—is that external dependencies and other high cost interfaces can be delegated to their own method or instance variable then called. In general, I avoid inheritance if there is a way to approach a problem with composition, but testing via fakes are much more reliable to make by inheriting the original class and overriding a method or setting a default initialization parameter then calling the rest of the constructor. This takes the pain out of so many parts of testing that it's truly hard to understate.[0] For example, say a library has a class that calls out to a third party service like S3 and an instance of that class is used in a business object in order to persist data. By introducing a fake that inherits from the library's class but overrides it's persistence method to effectively be a noop the rest of the validation, calculation, and other side effects that the instance does is conserved without the downside of waiting on a network call. It's not perfect, for example it could miss validation or side effects that only exist on the external service (e.g., S3 could change a mime type based on some crummy AWS logic) but it's a hell of a lot better than a mock that constantly lies to you about what its done. Furthermore, it's actually extremely simple to code up, unlike pure fakes that need to return hard coded values for certain method calls. I know that something akin to this is theoretically possible in functional languages, but I've found that in practice it's more difficult. Though I appreciate other areas where functional languages shine, including situations with testing. It's kinda hard to discuss because it really depends on so much context and good judgement and I've seen truly great development teams use languages like Scala in situations where I would have used Python with Numpy / Cython for speed. So don't take this as some hard and fast pronouncement. Just the general observations of someone that's been coding for decades and is reasonably well paid. [0] I think the primary reason that I prefer integration testing over unit testing is that I've adapted to its shortcomings by the above approach.
- etripe 6y agoThe benefits you describe for OOP can be achieved in FP through function composition. DI doesn't have to mean you set up a container, you know. > I know that something akin to this is theoretically possible in functional languages, but I've found that in practice it's more difficult. Could you explain what you think is hard about it?
- deleted 6y ago[deleted]
- exabrial 6y agoI don't think he really sells the point to me. You can use any tool incorrectly. An application with poor composition conmbined with mocks without verifications leads you down the path he describes. What we should do is only use "mocks to create fakes", which if you read the manuals on Mockito, is what they're pushing you towards anyway. So in conclusion I would say that a lot of people use mocking frameworks incorrectly and the author is hitting home on that.
- srtjstjsj 6y agoYou only use mocks to create fakes if you are stuck with mocks and you are learning along with the Mockito authors that fakes are better.
- SassyGrapefruit 6y agoI watch more people overcomplicate their lives when it comes to unit testing. The problem discussed in this article is not one of mock vs. fake its actually about how this individual chose to engineer their application. First I'll introduce some axioms... 1. Mixing unit and integration testing is not ideal. If you are doing unit testing focus on verifying the code paths within the unit. When considering the collaborators focus on the finite number of states that are most likely to occur. 2. (I know this is contentions) Design your code so that it is easy to unit test. It'll be ok I promise. In almost all cases code designed for easy unit testing is synonymous with well engineered code Imagine this python... ``` def get_a_file_and_do some_stuff(path): d = dict() with open(path, 'r') as file: for line in file: d[line] = d.get(line, 0) + 1 # a bunch of code that does something with this dictionary return result ``` The above is miserable to test with mocks. You end up trying to man handle the file object. Its not fun. ``` def get_a_file_and_do some_stuff(path): d = convert_path_to_dictionary(path) # a bunch of code that does something with this dictionary return result ``` With the simple flick of the wrist I can now trivially mock out "convert_path_to_dictionary(path)" and I only ever have to work with dictionaries. So you might say "but but what about the file dictionary code. are you going to test that?" Probably not and if experience is any indicator I'll never have an issue with it. The edge cases and regressions will all lie in the custom business logic executed on the dictionary. I see engineers make their lives enormously difficult to live up to some unachievable standard. Often that standard yields very little value in excess of a much simpler approximation
- byte1918 6y ago"All problems in computer science can be solved by another level of indirection" - David Wheeler I agree with you for most part. Mixing unit tests with integration tests is a recipe for failure.
- srtjstjsj 6y ago"Making code more testable" by moving the logic into functions you don't test, is not a winning strategy.
- 6y ago
- bluGill 6y agoThere is another advantage of good fakes: they often can be shipped in production as your demo mode. I've seen cases where when of the most important new features to marketing was just make some part of the fake more useful in production. Sometimes because there is a show coming up and the demo mode is what customers will see. Sometimes because the trainers want a better simulation of some issue. In some cases the fake is a lot more complex than the real implementation. (I worked on a diagnostic tool, the real implementation "just" needed to send/receive bytes, the fake needed to understand the bytes and return responses)
- Chris2048 6y agoJust to comment on the topic at a higher level: I'd like to add my own thoughts to this discussion, but feel it's the kind of topic that blows up into a mess/mass of opinion such that adding another comment just adds to the dogpile; after a certain critical mass, no-one really reads the majority of comments before commenting themselves, and a positive feedback of repetition begins. Maybe if the discussion was broken into narrow sub-topics this could be fixed?
- sethammons 6y agoI've written similar: https://sendgrid.com/blog/when-writing-unit-tests-dont-use-mocks/ https://sendgrid.com/blog/when-writing-unit-tests-dont-use-m... So has Martin Fowler: https://martinfowler.com/articles/mocksArentStubs.html https://martinfowler.com/articles/mocksArentStubs.html The TL;DR: Mocks increase coupling and make systems harder to change. Fakes are more versatile.
- jackcviers3 6y agoTo play devil's advocate: What exactly is the difference between a Mock and a Fake? In one you store any side effecting values in memory with a predefined behavior in a separate class. In the other, you define the predefined behavior in the test class by intercepting calls to external components. Your test coupling to internal implementation is just as high, or higher. Your Fake needs to do exactly what you expect in isolation of the class that depends upon it, or the test result won't work. In addition, without the reflection capability provided by many mocking frameworks, it is impossible to "Fake" implementations of certain library components via `sealed` or `final` modifiers on external library classes. It is also impossible to verify side effects without a method to expose those side effect actions in your Fake, which will force, at the very least, an extended declaration in your test class. Integration testing is an essential part of the test pyramid. When your required interface for your injected mock/fake changes, your test is going to have to change. Your tests will be just as coupled to your implementation with a fake. The coupling is just hidden in your fake implementation. In addition, often we are mocking huge interfaces with complex inner machinery that we don't fully understand. The real issue with mocking and faking is the necessity to mock and fake to isolate tests due to poor interface design in the class under test, usually focused around side-effecting methods. All side effecting methods must return some sum type that is not void that indicates success or failure of the side-effecting actions. All side effecting actions taken by the code must be apparent at the type level of this success or failure wrapper. Tests must use random, generated input parameters to verify the returns generated are correct. And as always, a type signifies for all X, then Y; while a test signifies for this x in X, then this y in Y. Types are enforced by the compiler or runtime, which (though they may have a bugs themselves) are never wrong as the interpreters of code. What you get is what you get, even if it doesn't match up with what you think a particular type expression should output. Even with effect encoding and property-based generated testing and compiler/runtime proofs that your program interfaces are correct, it is highly unlikely that a person incapable of designing and implementing a correct implementation would be capable of designing and implementing a correct test for that implementation or type hierarchy. Even with effect encoding at the type level all the way down, integration testing between components against live infrastructure remains the best way of detecting bugs -- all your unit testing and design assumptions are exercised by exhaustive business case testing. Mathematical proofs derived from the type system are very strong validators of design. But they can only verify that the assumptions and constraints the designer has created are correct. Only verification via integration and user testing can verify that those assumptions and constraints actually meet the business requirements of a given design. All of this is to say, if you are focusing on using Fakes over Mocks and relying upon tests to ensure correct behavior, eventually you will fail. In other words, bugs will eventually manifest in any system in which automated integration and user testing that verifies exhaustively the business cases requested by the business user is not part of the release process. Unit tests and types are measures of the correctness of a given design with respect to itself. Unit tests and types are not the measure of the reliability of a given design with respect to the use of software artifacts, and cannot be. Nor can coupled/uncoupled unit tests improve the ease of maintenance of a program. Types and unit tests are a useful design and maintenance tools that can make the intent of a design clearer to the reader of a piece of code, and can help identify times while implementing where you are violating the constraints you have set for yourself. But they cannot make refactoring or interface changes easier to encode without inflicting line churn cost in the tests. If an injection is exposed to the user of a class, it is exposed, and you must deal with it.
- aszen 6y agoI have been researching into several testing approaches and so far in my experience yes fakes are better than mocks. But what is even better is not having to fake or mock stuff especially for testing business logic. One of the problems we get ourselves into is abstracting service io code into functions and calling them from our business logic code. This forces us to mock all the io calls, instead we should be doing the opposite i.e abstracting business logic into discrete functions of pure data in and out and then calling those functions from our io code. This way we can test the business logic independently of the io code. Testing the business logic now becomes quite simple and one can leverage property testing to further boost our confidence in such tests. Many times the ramaining io code is just calling other functions and doesn't need to be tested. Several other ideas that relate to this are hexagonal architecture, Domain Driver Design, clean architecture and functional core with an imperative shell. Some resources to further explore these ideas: Clean Architecture in Python - https://youtu.be/DJtef410XaM https://youtu.be/DJtef410XaM Why integrated tests are a scam - https://youtu.be/VDfX44fZoMc https://youtu.be/VDfX44fZoMc Boundaries - https://youtu.be/yTkzNHF6rMs https://youtu.be/yTkzNHF6rMs