2

Firstly let me say I have never written a unit test in my life. I am trying to get the hang of PHPUnit, and so far it's working pretty well for me so far as "stateless" functions (that simply take in an input and return an output regardless of whatever else in the programme is going on, and create no side-effects) are concerned.

I am, however, at a complete loss when I start looking at my functions that aren't stateless. My knowledge of best-practices is limited, and tutorials treating this subject simply pretend that every function ever written is stateless and can just be evaluated by the value of its output given a few parameters. But what if the function performs some interaction with a database? Should I create a "dummy" database structure, so that every time the test programme runs, a new database is created out of thin air, filled with dummy data, manipulated by the function(s) I'm testing, and then deleted again (probably repeating this whole process several times, as I'd need more than one test case)?

Or, to give another example, what if my function is communicating with the user's browser - say, session data? Should I create a new session, fill it with placeholder data, and test my functions that way? What about cookies, which require two executions (since cookies are filled after the response is sent)?

And if I'm testing error cases, where the software should detect and throw an exception - the only solution I've managed to come up with is to perform the test, check the error log for the expected error message, and then delete the last error message from the error log. Surely this isn't the best way to test error cases?

I'm most likely missing something obvious, or fundamentally misunderstanding how unit-testing is supposed to work (apologies if so), but I'm at a complete loss as to how to handle unit testing for units that require a whole external framework to be set up (if the function is manipulating data in a database for example).

Any insight on this would be much appreciated. Thanks!

3
  • 1
    An (almost) antonym of how you use "stateless" is side effect - code that modifies state outside of itself is said to have side effects. Note that code with side effects can still be classified as a unit test according to the Michael Feathers definition if it still follows the same rules outlined in candied_orange's answer. Commented Feb 13 at 21:58
  • 1
    Idempotence is the computer science term for what you are describing as "stateless". I don't think "stateless" is wrong, though. Commented Feb 13 at 22:01
  • 1
    Idempotence is the property that performing an action once, twice, or a million times has the same end result. For example “change customer name to John Smith” but not “give one dollar to John Smith”. Nilpotence is the property that performing the action zero times also has the same outcome.
    – gnasher729
    Commented Feb 15 at 8:03

2 Answers 2

3

Testing is more than unit testing. Testing can be as simple as running the code and playing with the app. It can be as intense as a suite of tests ran before you're allowed to merge code into a baseline. And many things in-between.

And just because you run a test under PHPUnit doesn't necessarily make it a unit test. It may be a different kind of automated test.

So when you ask, "how unit-testing is supposed to work" I feel the need to define what unit-testing is and isn't. At least to me.

The best definition I've found comes from Michael Feathers:

A test is not a unit test if:

  • It talks to the database
  • It communicates across the network
  • It touches the file system
  • It can't run at the same time as any of your other unit tests
  • You have to do special things to your environment (such as editing config files) to run it.

A Set of Unit Testing Rules

Following these rules allows your test to have important features. Unit tests should test the behavior of your core code. They should be deterministic, parallelizable, and fast.

When you talk about "stateless" functions what you've really identified is the need for the functional unit under the test to be deterministic so the test can be as well. Deterministic is just a fancy way to say always does the same thing. And yes, state, DB reliance, shared mutables, random, and cosmic rays can all prevent deterministic behavior.

The classic way to handle that is to draw the boundary of the unit so that the things that keep the test from being deterministic aren't part of the test. What we want tested is behavior, not the DB.

Making that possible often requires writing testable code. That is, code that allows behavior code to be isolated from non-deterministic code. This can be done with wise architectural choices. In a pinch it can also be with mocking.

Mocking has fallen out of favor. Not because it's wrong. Mostly because it's easy to over use and avoid making good architectural choices.

A pattern that encourages good architectural choices is imperative shell, functional core. This habit keeps your behavior code in nice functional deterministic code that is easy to test. The imperative shell gives you a place to put that other code somewhere it wont get in the way of the unit test.

Sounds like a lot of work but the sweet thing about this is if you have the behavior under unit test then your Integration Tests (that can cover the whole system) don't have to focus on behavior. They just ensure that everything else, that you had to cut out of the unit test, is plugged into the core correctly. Since they deal with IO, Integration Tests are typically slower. But mercifully, if you do this right, they will be fewer.

That pattern is nice but it can be defeated by putting business rules in the DB. This testing pattern only works when behavior code can be isolated.

So yes, you can create mock DBs that have test data. If you're going that way consider an in memory DB for testing to speed things up. But understand this is not the architecture that makes unit testing thrive.

The way to deal with a DB in the functional core way is to push DB communication out to the imperative shell. Hide the behavior code in deterministic functions that get their state from how you call them (or how you construct their objects if we're talking methods; they can be deterministic as well). That way they can be given state without a DB necessarily being involved at all.

Do that and your tests can deal with state just fine.

2
  • 1
    Not that you shouldn't write tests which do those things, you just need to describe them as "integration tests". And it can be very valuable to have some automated end-to-end tests. It's just that they usually take longer to run and require different scaffolding from true unit tests.
    – pjc50
    Commented Feb 14 at 10:43
  • @pjc50 it’s more than just what you call them. It’s what you have them test. Commented Feb 14 at 14:24
2

You have two choices. First let’s assume that the code you are testing is not accessing the database directly, but there is some class in between. And let’s assume there is one place in your code that finds and opens the database.

First, you can create a mocking class that pretends to access the database. That mock class pretends to have a 20,000 item database but it’s only pretend. The mock keeps track of all changes. So you add a record through the mock object, it will tell you there are now 20,001 records. Start the unit test tomorrow and you get the exact same mock database.

Or you just create a new database from scratch and fill it with tons of data before your unit tests start, then use the original database object.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.