TDD anti patterns
TDD
Test Driven Development means first we need to write a code that fails, and then we have to write the code.
The steps are
- Write failing tests
- Make the test to pass
- Refactor
Repeat these steps again until we get the required feature.
Anti-pattern
It is a term that describes how NOT to solve recurring problems in your code. Anti-patterns are considered bad software design, and are usually ineffective or obscure fixes.
We will see some anti-patterns that won't occur if we follow Test Driven Development.
The Liar:
Liar is a unit test that runs and doesn't fail. But it doesn't test, what it is meant to test.
Example
@Test
public void testCheckSignShouldReturnNegative() {
MySimpleMath sm = new MySimpleMath();
Assert.assertEquals("positive", sm.checkSign(5));
}
As we can see in this example the testCheckSignShouldReturnNegative
is supposed to check whether the returned value is "negative" or not, here the test will pass but it checks for "positive".
Excessive Setup:
It is a scenario of testing that requires a lot of setup works, even to begin testing.When several lines of codes are needed in setup to test a single method it will be difficult to know what we are testing.
Such a scenario can be like: for an application facilitating online meetings. To test whether a user of particular type, can join the meeting, the steps to test are:
- Create the User
- Set user permissions
- Create the meeting
- Set meeting properties
- Publish meeting joining information
- [Test] User join the meeting
- Pass/Fail
Here around five steps are needed to setup for testing one method. The solution to solve this anti pattern will be using Helper methods and TDD mocks.
The Giant:
Although a unit test is validating a single object under test, it has many test cases and many lines of code.
public class SomeUserGodObject {
private static final String FIND_ALL_USERS_EN = "SELECT id, email, phone, first_name_en, access_counter, middle_name_en, last_name_en, created_date FROM users;
private static final String FIND_BY_ID = "SELECT id, email, phone, first_name_en, access_counter, middle_name_en, last_name_en, created_date FROM users WHERE id = ?";
private static final String FIND_ALL_CUSTOMERS = "SELECT id, u.email, u.phone, u.first_name_en, u.middle_name_en, u.last_name_en, u.created_date" +
" WHERE u.id IN (SELECT up.user_id FROM user_permissions up WHERE up.permission_id = ?)";
private static final String FIND_BY_EMAIL = "SELECT id, email, phone, first_name_en, access_counter, middle_name_en, last_name_en, created_dateFROM users WHERE email = ?";
private static final String LIMIT_OFFSET = " LIMIT ? OFFSET ?";
private static final String ORDER = " ORDER BY ISNULL(last_name_en), last_name_en, ISNULL(first_name_en), first_name_en, ISNULL(last_name_ru), " +
"last_name_ru, ISNULL(first_name_ru), first_name_ru";
private static final String CREATE_USER_EN = "INSERT INTO users(id, phone, email, first_name_en, middle_name_en, last_name_en, created_date) " +
"VALUES (?, ?, ?, ?, ?, ?, ?)";
private static final String FIND_ID_BY_LANG_CODE = "SELECT id FROM languages WHERE lang_code = ?";
........
private final JdbcTemplate jdbcTemplate;
private Map<String, String> firstName;
private Map<String, String> middleName;
private Map<String, String> lastName;
private List<Long> permission;
@Override
public List<User> findAllEnCustomers(Long permissionId) {
return jdbcTemplate.query( FIND_ALL_CUSTOMERS + ORDER, userRowMapper(), permissionId);
}
@Override
public List<User> findAllEn() {
return jdbcTemplate.query(FIND_ALL_USERS_EN + ORDER, userRowMapper());
}
@Override
public Optional<List<User>> findAllEnByEmail(String email) {
var query = FIND_ALL_USERS_EN + FIND_BY_EMAIL + ORDER;
return Optional.ofNullable(jdbcTemplate.query(query, userRowMapper(), email));
}
private List<User> findAllWithoutPageEn(Long permissionId, Type type) {
switch (type) {
case USERS:
return findAllEnUsers(permissionId);
case CUSTOMERS:
return findAllEnCustomers(permissionId);
default:
return findAllEn();
}
}
private RowMapper<User> userRowMapperEn() {
return (rs, rowNum) ->
User.builder()
.id(rs.getLong("id"))
.email(rs.getString("email"))
.accessFailed(rs.getInt("access_counter"))
.createdDate(rs.getObject("created_date", LocalDateTime.class))
.firstName(rs.getString("first_name_en"))
.middleName(rs.getString("middle_name_en"))
.lastName(rs.getString("last_name_en"))
.phone(rs.getString("phone"))
.build();
}
}
This single God Object class will be the best example. It has
- Database queries
- Data for Storage
- FindAll methods
- Some Business logic methods
The testing of this object will have many test cases and many lines of code. The solution can be given for writing the test cases first and then write the code, which results in using different classes for DataBase queries, FindAll methods and Business logics each.
The Mockery:
Mocks can be good top some extend, but over use of mocks won't be helpful. If a unit test has many mocks the data from those mocks will be tested but not the mocks.
The Local Hero:
A test case that is dependent on an environment that it is developed. The test will pass on the machine that it is developed but fails on other machines.
There are many anti-patterns like these which can be solved when we follow Test Driven Development.
Written by Aravind P C