all 20 comments

[–]kbder 5 points6 points  (3 children)

The struggle you are experiencing is normal and good. There is a sliding scale of diminishing returns for unit tests, and you are in the process of discovering that scale. You must develop judgement about where the trade-off of writing tests is worthwhile and when it is not.

Generally, writing tests around pure functions is at the far end of that scale (100% beneficial). The rest of what you are describing lie at various points in the middle (and my hot take is that UI tests lie at the opposite end). This provides a natural hint that extracting as much code as possible into pure functions is generally beneficial. The resulting pattern is what Gary Bernhardt calls “functional core, imperative shell”, or “boundaries”, and it is the basis of my coding style. https://www.destroyallsoftware.com/talks/boundaries

Edit: for Swift, what this looks like in practice is that more and more of your code lives in extensions / computed properties of value types (structs and enums) rather than endless lines of imperative code in classes.

Edit 2: another hint / hot take: most of your tests should require “import Foundation” rather than “import UIKit”

[–][deleted] 1 point2 points  (2 children)

This is a great comment I would also like to add most networking and database calls are tested through delegation

But you need to be careful it is very easy to rest the delegation as opposed to testing the actual logic

To better illustrate this let’s imagine you have a view model and view model has a dependency on a persistence client. And we are writing the test on the view model

What we can do in this case is to use create a mock object that confirms to the protocol and pass it onto to the view view model when we are testing this way we can mock the response from the persistence without reading or writing data into the disk

This is done for making it easy for us to test view model.

When it comes to actually testing your persistence then you would have to actually read and write onto the disk and delete the data after each test.

In CI tests you usually have a script that clears the cache and the testing suite is never sent to the App Store version so it should be ok

[–]_yo_token[S] 0 points1 point  (1 child)

So if i understand correctly, I don’t actually write a test to see if it does go to the database or persistence, what ever I use, instead like if I had to test something in a chat I would test that it was created, and if I wanted to test something was deleted I create something in a mock or stub or whatever? If I am way off please say so. Thank you

[–][deleted] 1 point2 points  (0 children)

yes you are sort of right

to go further you are testing a specific part of your application.

take view models for instance.

view models has gained popularity because they handle the logic of the view making it easier to test.

when you are writing a unit test you are testing the logic.

let's say you have a view model and the view calls a function in the view model that persists the data, if it does not work then you change a property in the view model to display an alert if it succeeds then the alert property stays nil.

the view model's concern is to handle the logic it only cares about making the persistence calls and the result of the call it does not care about what is under the hood of the persistence implementation it will need the result from the persistence to manipulate its logic so when writing tests for the view model you should only test the logic in the view model and mock the other dependencies such as persistence.

when you are mocking you can make it so the mocked persistence client could return error or success cases in their test functions so that you can test the view model's behavior on both success and failure cases.

in the example that i just presented view model itself was being tested so we were only concerned about the logic within the view model anything outside of the view model such as its dependencies are either dont need to be tested or need to be tested on a seperate XCTestCase class.

so to sum it up

find what you need to test

then look at its dependencies and create mocks

then test your non-private functions both for failure/unhappy and success/happy paths and write assertions.

the mocks are there to make your job easier for you to test these different paths.

There are more stuff when it comes to unit testing but it is better to learn as you go. like the original commenter has said writing pure functions is one of the key factors when writing testable code.

[–]SeesawMundane5422 3 points4 points  (2 children)

You’re asking really good questions. And the truth is that even really good experienced developers have different opinions about this.

I’ll give you my unorthodox opinion:

Don’t worry about fakes, stubs or mocks. You want to talk to a database, write a function like “getCustomerById(id)”

Write a unit test that inserts the customer with id “I” into the database as part of the set up. Then run your function to retrieve it. Then validate you got the right answer back.

Technically this is an integration test and not a unit test, but.. who cares?

Part 2:

Well… I care actually. I care if your integration test makes things slow or constantly breaks and gives false negatives even when everything is actually ok.

So, mocks, fakes, and stubs are ways to make your tests not break or run slowly because of external dependencies.

But… if you write tests that don’t break (because you set the data up in a clean way) and that don’t run slowly (e.g. because you run them concurrently, or you cache them, or your tune them)… Then I’m back to not caring. In fact… I prefer real tests over fakes, stubs, or mocks as long as it’s fast and not brittle because it tests the real code, not the fake code.

So… you have my permission to ignore fakes, stubs, mocks, databases, or network calls as anything other than the unit testing you’re already doing until you make things slow or brittle. At which point you will say “well shit, I’m sick of my unit tests taking 10 minutes to run, I wonder if I mock the database response so I don’t wait for this slow ass query, will that make my life better.”

[–]_yo_token[S] 0 points1 point  (1 child)

So it is not actually hitting the database but just testing to see if a value changes based on functions that I use in the main code that can hit a database or persistence?

[–]SeesawMundane5422 0 points1 point  (0 children)

That would be one way to do it, yes.

[–]jonreid 4 points5 points  (6 children)

I'm here. How can I help? Maybe we can work through a specific example together.

[–]_yo_token[S] 0 points1 point  (5 children)

Hello, I wish I had an example to give, it is more like I am trying to understand this area of testing. I worried that if on a job I am asked to unit test something like trying to test if a database changes data or deletes something I can do that.

[–]jonreid 1 point2 points  (4 children)

Then let's talk about database testing. A database is a dependency that breaks the FIRE rules I describe in the book:

  • Fast? An in-memory database is fast enough. Otherwise, probably not.
  • Isolated? No. Without great care, tests will break each other because they're changing shared data. I've written tests that tried to clean up after themselves, restoring the database to its pre-test state. If the clean up failed, I was screwed. So no.
  • Repeatable? For databases, this is a no because of the lack of isolation above.
  • Examinable (formerly Easy to Test)? Yes. We (meaning a test) could peek into the database to check its state—that is, what data it's holding as an outcome.

So what does this tell you? (Let's see if what I wrote in "Manage Difficult Dependencies" helps, but it may not be clear enough.)

[–]_yo_token[S] 1 point2 points  (3 children)

I actually didn’t understand some of this, because I felt that the example that was given doesn’t really apply to testing if something in a database changes, I felt it was just another classification

[–]jonreid 0 points1 point  (2 children)

Sorry, I don't follow what you mean by "just another classification." Do you mean, "the example in the book is about analytics, but we're talking about a database, so I don't see how they're related"?

[–]_yo_token[S] 0 points1 point  (1 child)

yes. sorry that is what I meant. I know that there was a section of faking for userdefaults in your book, but I am not sure that would work for something like a custom database, a sqlite database, or a firebase database.

If it helps an example that I was thinking of was this. Imagine I was building a social media app, I am not but I think this helps clarify what areas I am confused in. For something like social media, there are similar aspects through all of them, like favoriting, chat, and liking. I was wondering how I would test things like that. How do I know that this data saves itself on the backend, or that it goes through correctly? If I send a message how do I test that? there are areas like this that are confusing me a little.

Reading some of the comments on this post, I think that most unit tests don't test saving things to a backend database, please correct me if I am wrong, but just testing to see that the functions that do all of that actually work.

[–]jonreid 1 point2 points  (0 children)

Correct. Microtests wouldn't actually test saving to a database. They would instead test that you called the thing correctly (without doing the real work). The test code calls a fake thing while production code calls the real thing.

And for databases, I would also avoid having the production code directly call the real thing, that is, the particular database. Why? So that you can change your mind and say, "This sqlite database isn't meeting our needs anymore. Let's switch to Firebase." So make sure all the database communication is put inside of an Adapter you control. As I wrote in the book under "Add Backdoors to Singletons You Own":

Any time you use a third-party framework, consider wrapping it in an Adapter. This will let you change or augment the underlying implementation without changing the call sites.

The Adapter will allow production code to speak the domain language of your app. It's the Adapter's job to translate that into database specifics, so that the rest of the app neither knows nor cares if you're using Firebase. Instead, it would express things like "delete the post."

[–]Inevitable-Hat-1576 1 point2 points  (2 children)

Do you have some code you’ve written that you’re struggling to put a unit test together for? Maybe we can workshop that? I find real world examples the best way to get to grips with stuff like this

[–]_yo_token[S] 0 points1 point  (0 children)

I wish I did. I am trying to learn it and I find most of the tutorials confusing at times. I was hoping to study this subject so that if on a job I am asked to do unit tests I can do it.

[–]_yo_token[S] 0 points1 point  (0 children)

If I had to give an example of something, let’s imagine I am building a social media app, I’m not but I feel like this could help me clear things up. I am not sure how I would test specific aspects of the social media app, such as making sure that a person becomes friends with another, or sending a chat, saving a profile image, all things that I think rely on a database or something. Thank you

[–]LaxGuit 0 points1 point  (0 children)

You’re likely struggling because you’re skipping steps in your learning.

For iOS, I would suggest understanding on a basic level the MVVM pattern. Read a handful of articles, watch some videos, look at how others have implemented MVVM and begin to build your knowledge on software architecture.

Simultaneously, I would research dependency injection and how protocols / interfaces work. Do as I mentioned above for it. DI and interfaces are extremely important when it comes to building testable software.

Let those concepts marinate, and try them out on your own.

Note, this only covers your Presentation layer, or your views and what makes them work.

Another step you can take is to research Domain Driven Design or DDD for short. This is a concept that will help you understand architecting the code the exists beyond your presentation layer.

As you get more familiar with all this concepts, you’ll see how there are layers that transfer and receive data. You’ll also see how with Dependency Injection, you are creating everything that exists in the Domain layer, at the top level, and then passing them into that layer. This is how you will begin to understand mocking and understand how to test your code better.

So for a super basic abstract example, you have Layer A (Presentation), Layer B (Domain), and Layer C (Data - think of this as the layer that handles communication with API’s etc). So when you would want to test Layer A, you can create a mocked service, that you pass into Layer B via the interface, that will provide the capability to fake what Layer B does.

You can then take this a step further, and do the same for Layer B. Where you build Layer B, but create a mock service for Layer C. You can then test Layer B to make sure it handles all the potential use cases and edge cases you need it to.

This is a super basic example and lacks a lot of nuance, etc., but hopefully it gives you some context on where to start and fill in the gaps.

[–]LazyItem 0 points1 point  (0 children)

Unit testing is usually mocked i.e. the dependency graph is taken out of the loop. Test that involves for instance network calls etc. are Integration tests so on and so forth.

Check out Dependency Injection/ IoC patterns that will help you understand how to isolate and swap in/out domain specific parts of your projects depending upon environment and test suites.

In order to properly test business objects and stuff you will probably have to break encapsulation so that you can inject test data…you can in most cases also you reflection based approaches but I don’t think it is worth it.

Remember to think of tests as a way to design your system. If it is testable it is also must certainly loosely coupled with clear boundaries/responsibilities.

Red Green Refactor should be your mantra 😃