Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # Anti-tests
- What follows is a real world example that I've come across more than once in a professional context.
- ## The requirement
- Let's say we've been tasked with returning `400` when `GET` `/users/<userId>` is called with a negative `userId`.
- ## Integration test
- The requirement can be turned into an integration test that actually hits the endpoint and checks that a `400` is returned:
- ```java
- @Test
- public void getUser_InvalidUserId_400() {
- expect().statusCode(400).when().get("/users/-1");
- }
- ```
- ## Implementation
- A ubiquitous style of implementation may look something like this: (you may or may not like it, but it's ubiquitous)
- ```java
- public class UserResource {
- @Inject
- private UserService userService;
- @GET
- @Path("/users/{userId}")
- public Response getUser(@PathParam("userId") final Long userId) {
- try {
- return Response.ok(userService.findById(userId)).build();
- } catch (final UserException e) {
- return Response.status(e.getErrorCode()).build();
- }
- }
- }
- ```
- ```java
- public UserService {
- @Inject
- private UserDao userDao;
- public User findById(final Long userId) throws UserException {
- if ((userId == null) || (userId <= 0)) {
- throw new UserException("Invalid arguments", 400);
- }
- return userDao.findById(userId);
- }
- }
- ```
- ## Unit tests
- Everyone knows integration tests are not enough- unit tests are required as well. A ubiquitous style of unit test may look something like this:
- ```java
- public class UserResourceTest {
- @InjectMocks
- private UserResource userResource = new UserResource();
- @Mock
- private UserService userService;
- @Test
- public void getUser_InvalidParams_400() throws UserException {
- doThrow(new UserException(INVALID_ARGUMENTS, 400)).when(userService).findById(-1L);
- assertThat(userResource.getUser(-1L).getStatus(), is(equalTo(400)));
- }
- }
- ```
- ## Test the tests
- ### Break something
- Let's say the negative `userId` check is removed from the `UserService`. (by mistake or whatever)
- The integration test will fail, because it will no longer get a `400` when `GET` `/users/<userId>` is called with a negative `userId`. This is exactly what we want out of a test!
- The unit test however will pretend the negative `userId` check is still in the service and pass. **This is literally the exact opposite of what we want out of a test!**
- ### Refactor something
- Or, instead, let's say the `UserResource` is refactored to use a `NewBetterUserService`. (the `NewBetterUserService` still throws an exception on negative `userId`)
- The integration test will pass, because it will still get a `400` when `GET` `/users/<userId>` is called with a negative `userId`. This is exactly what we want out of a test!
- The unit test however will fail because it expects the `UserResource` to call the (old) `UserService`. **This is literally the exact opposite of what we want out of a test!**
- ## TLDR test interfaces, not implementations
- Anti-tests like the ones described are worse than useless- they are positively harmful.
- ## Misc
- There are other problems with the example, such as eg
- * Primitive Obsession: using a Long to represent `userId` and using procedural inline checks in the service to check for negative `userId`. Instead, create a class `UserId` that encapsulates and enforces its own checks.
- * Hidden Dependencies: `UserResource` has a hidden dependency on `UserService` and `UserService` has a hidden dependency on `UserDao`. Dependency Injection frameworks like Spring encourage such hidden dependencies, and the use of mocks in unit tests.
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement