User centric tests are simper and more effective than you think

We developers just love writing automated tests, don't we? (Except for the guys and gals who believe that tests are for testers only). We also love writing tests before we actually write code, because it is still a revolutionary concept for us, and we like doing absurd things. We also call it "TDD" in our blog posts.

While we love to call it an Art, actually we're quite happy that it is not. We're not artists, after all. Not those spaced out wierdos always having late noisy guests and borrowing money from us the day after. We love it to be predictable.

And that's why we love programmer-centric tests.

Now, if somebody tells us about user-oriented tests, and maybe even mentions BDD casually, we just imagine that we'll have to write an end-to-end test that would involve a database, simulating user mouse clicks, and other scary stuff, including late night debugging test ordering issues.

Fear not.

I'll give you a real world example of a user-oriented test that it going to change your life forever. Or maybe not. But it will sure be more robust, less complicated, easier to support, and better documenting the system's functionality than its programmer-oriented version.

(On a side note, do I sound like I'm selling you something? Must be reading too many marketing blogs..)

Let's start.


Recently I've been working on a big solo project of mine, a .Net code editor called Chpokk. Naturally, since the code it edited online, it must be stored somewhere on the server. My first idea was to put each user's files in a folder that's named after her username. But there's one tricky point. Since I'm using Janrain for social login, there can be different users with the same username. For example, my Twitter's username is uluhonolulu, but somebody could register a WordPress account with the same name and get access to my files. So, I had to think about something unique.

Fortunately, Janrain gives me a lot of data, including something called uniqueIdentifier. Which is, well, unique, so it's a good candidate for a folder name. Since it can contain symbols that are unacceptable for a folder name, and since I wanted to keep the user's name in the beginning (so that I could quickly find the code and investigate any issues), I decided to go with the following: let the folder name be {username}_{hash of the unique identifier}.

And now the testing fun begins.


See, since I already know the implementation, there's a hard-to-resist temptation to write a test that assumes this implementation. A typical developer-centric test has implementation details exposed all over it. In our case, it would look like

public void GetFolderNameShouldReturnTheCorrectValue() {
	var userName = "uluhonolulu";
	var id = "xxxyyy";
	var folderName = GetFolderName(userName, id);
	Assert.AreEqual("uluhonolulu_-1493874745", folderName);

There are so many things wrong with this test. But the main problem is, it is hard to understand what it checks, and what part it plays in the application. Imagine that a year later we change some part of the code and the test breaks. Is it good or bad? Should we fix the test, or the production code? If folderName becomes something other than "uluhonolulu_-1493874745", would it break some of the app's functionality? The tests are meant to provide, among other things, a safety net, but in this case the net is so tight it acts like a straightjacket. Why? Because it has my idea of implementation hardcoded both in the inputs and the expected output, any time the implementation changes (and it will change), we risk breaking the test even if the functionality is OK.

My favorite technique for fixing the tests (or writing them the "right" way) is to start with the name. From the user's perspective, it is not important how we are constructing  the folder name, what's important is that the folders are different for different users. So, let's change the name to:

public void DifferentUsersShouldGetDifferentFolderNames()

Now, we have to just code what this name suggest, meaning we have to take two different users and compare their folder names. Note that we don't write an end-to-end test, we are still in the unit test category. So, our Assert stage should be something like this:

Assert.AreNotEqual(firstFolderName, secondFolderName);

Next, let's get to the Act stage. The signature of the GetFolderName method that we suggested before just doesn't fit here. it leaks implementation details. Meaning that just looking at it, we already know something about implementation (namely, that it is based on username and unique ID). And that's just as bad as making all your private functions public. Only worse. Because, once you make it public, it is very hard to change it.

So, let's get back to our initial requirement, and forget our idea of implementation (that's the hardest part of TDD, even if you know how to implement something, you force your mind shut up and listen to the tests). What we have to calculate is the name of the folder, unique to the user. What we know about the user is just the user profile data. So, let's use it as the single argument for our method, and the method itself would be responsible for getting the fields that it needs.

See what has just happened here? They say TDD helps us write better code. We've just seen how it helped us find a better signature for our method.

Our Act part of the test becomes

string firstFolderName = GetFolderName(firstProfile);
string secondFolderName = GetFolderName(secondProfile);

Going backwards, we are coming to our Arrange stage of the test. It should be a little bit more complicated than the original version, but not much. We should create two sample profiles here with the same username. I could just make two simple profiles each having only username and uniqueId fields, but I prefer taking two real world profiles (say, two my profiles from different identity providers). Since we'll need a lot of code, I'm just refactoring it to a separate function (actually, two functions for two profiles), but I make it clear that they have the same userName:

var userName = "uluhonolulu";
dynamic firstProfile = CreateFirstProfile(userName);
dynamic secondProfile = CreateSecondProfile(userName);

Finally, here's our whole test:

public void DifferentUsersShouldGetDifferentFolderNames() {
	var userName = "uluhonolulu";
	dynamic firstProfile = CreateFirstProfile(userName);
	dynamic secondProfile = CreateSecondProfile(userName);
	string firstFolderName = GetFolderName(firstProfile);
	string secondFolderName = GetFolderName(secondProfile);
	Assert.AreNotEqual(firstFolderName, secondFolderName);
It is still a unit test, no complicated setup, no testing several things in the same test, in other words, nothing to be afraid of. And yet, it is clearly a user-centric test.


Remember that our purpose was to make this test less fragile. Meaning that it should still pass when we add more requirements (but keep the original one). Let's see whether we have achieved this with our new test.

One thing I discovered is that, as some sites use email for username, there's no "preferredUsername" field in the corresponding profile. I decided to change my implementation and use "confirmedEmail" for these providers. The test still passes -- no change required. The method's signature didn't need change, thanks to what our testing strategy suggested.

Second idea, what if I decide that users with the same email should be treated as one user? That means that I would need to change my implementation again -- same emails would mean same folder names. Would our test stay green? Well it depends on the input data. As I mentioned, I just took my profiles from two different providers. Note that I cheated: as I was testing the situation with two different people with the same username, I should have taken profiles of two different users. Well, when I'll be implementing my second idea, I'll be punished for my cheating, because my implementation will break that test (since I used the same email in both profiles, they now should correspond to the same folder, breaking the test).

Here are our benefits from converting this test to a user-oriented one:

  • It is clear which part of the system functionality this test ensures
  • The test will not break nor it won't compile if we introduce new specs
  • The test can serve as part of the executable documentation of the system
  • The test helped us design a better API


blog comments powered by Disqus

Latest blog posts

Powered by FeedBurner