The Importance of Negative Acceptance Criteria
Story time
Senior dev Sanjay worked for Agicorp. Agicorp was great, their development practices were modern and up to date, they had automated tests connected to their build pipeline, everything was verified as it went into the main branch, and they set up daily pushes of releasable builds based on their full automated test suite. Their technical and product functions were well integrated, so the devs got a good feedback loop with stakeholders, and could deliver value quickly and iteratively.
(Before we get too far into this I should put in this disclaimer, that Agicorp is made up and this story is fictional.)
Agicorp offered a back end solution as a web service. One day, they decided it needed a user-facing SaaS offering - a web UI for their service. Since it was a well run company on board with current trends, they wanted to build it in a product-led agile manner with good test coverage from the outset. It went well, they delivered value and got users on board, the system was active and there were no problems.
Until one day, a customer support ticket arrived: some data was going missing. Then, the customer found themselves locked out of their account entirely. Everyone relevant was brought in to investigate, including Sanjay.
What happened?
That was the first question, of course. Ollie, over in operations with access to the real system, sat down with Sanjay to look at the system's audit records. They showed that the data removal and account deletion had been done by a user "mike_119". A quick lookup against the users data store found a mike_119 as the owner of a recently created free tier account, apparently unrelated to the customer who raised the ticket. This user had logged in at the same time as the deletions. When Ollie brought up the web server logs all the delete requests to their back end API were clearly visible. It seemed like someone was using the admin pages, but of course it's hard to tell from such coarse grained logs exactly who or what.
"Oh no, that's not good", said Sanjay with classic developer understatement.
They took the system down to maintenance mode while he got together with developer David to work out how this could be possible. It turned out that although their interface and APIs were protected by authentication and session management, the API endpoints didn't properly check the role or account. If you were logged in to your own account and you knew information about another, you could swap your session to point to it, and keep your roles. If you were an admin on his free account so he could assume admin permissions on any other account if he knew their ID.
Turns out there was a Mike who had recently left the customer under bad terms, and he was senior enough he used Agicorp's service before leaving and took the account ID away with him. He made a free tier account so he could assume those permissions on his old company's account to mess with them.
Post-incident
Once they discovered the problem, the engineering team had to do a high pressure, quick turnaround patch to their entire API to make this kind of attack not be possible. It took Agicorp's service down for days, losing them significant reputation and business, and caused a lot of stress for everyone involved.
After the fire died down, the question began to be asked: how did we get here? don't we have full test coverage? And that's where we rejoin them.
What did they do wrong?
Agicorp was product and test driven. They engaged with the stakeholders, constructed stories with BDD acceptance criteria, and then wrote BDD-style system tests and unit and integration tests at the code level. Let's take a look at some simple stories for the first few pages in their application.
User can log in
As a normal user I want to be able to log in and browse the inventory.
Technical:
Create a login page that accepts a user name and password. If they are filled in correctly, the user should be able to log in be redirected to the inventory page.
Acceptance:
- Given I have a valid user account
- When I go to the app root
- Then I am presented with a login page with user, password and a log in button
- Given I am on the login page
- When I enter my correct credentials
- Then I am redirected to the inventory page
Admin user can manage account
As an administrative user I want to be able to log in and manage the account.
Technical:
Update the login process to include user roles. If the logged in user is an admin then show an option on the nav menu to go to the management dashboard.
Acceptance:
- Given I have a valid admin level user account
- And I am on the inventory page after logging in
- When I click the "management dashboard" item on the menu
- Then I am shown the account management dashboard
Those acceptance criteria were written up as executable BDD tests that were run automatically. We can imagine that they wrote some integration tests against their authentication provider as well:
领英推荐
@Test user_can_log_in() {
testAuthenticator.seedUser('user1', 'password1');
userResponse = authService.logIn('user1', 'password1');
verifyValid(userResponse);
}
@Test admin_can_log_in() {
testAuthenticator.seedUser('admin', 'password1', [ Role.Admin ] );
userResponse = authService.logIn('admin', 'password1');
verifyValid(userResponse);
verifyHasRoles(userResponse, [ Role.Admin ]);
}
All of these tests were green every day or every commit, the page looked great and worked as intended during their sprint review. So what's wrong with this?
Well, let's imagine the simplest solution we can write that passes all of these tests.
class AuthService {
logIn(String user, String password) : UserResponse {
return { valid: true, roles: [ Role.User, Role.Admin ] };
}
}
Now the problem should be obvious!
Negative Criteria
Agicorp's problem is that all of their test criteria are positive, i.e. they are testing that the behaviour that you want does happen when you are in the appropriate state. They have no negative criteria, i.e. they are not testing that the behaviour you specified doesn't happen when you aren't in an appropriate state.
In the worked example above, they didn't test that you didn't get admin role if you shouldn't. They didn't even test that you couldn't log in if your credentials were wrong!
I would hope that our fictional Agicorp wouldn't have been dumb enough to make this exact mistake. But they did make a similar mistake in their management page, not checking that users don't have access to resources if they are on the wrong tenant.
Important Types of Negative Criterion
There are a few different types of thing Agicorp should have thought of:
Aspects of a Negative Test
The essence of a negative test is "the system fails when you put it in an invalid state". But there's more to it than that: what does failing under control mean? You should think about
Do I Always Need Negative Criteria?
Anything but the simplest of user stories will have a way the user could do it wrong. So yes, you should always consider negative criteria. Almost every story will have at least "the user tries to access this without permissions", and anything where the user can provide content (in a text box, uploading a file or referencing a URL) needs to consider invalid user input.
Don't be Agicorp
I know some of you will read the top and then skip to the bottom :-) so here are the key actions you can take to make sure you include negative criteria: