DRY vs DAMP in Unit Tests

The DRY and DAMP principles

The DRY principle stands for “Don’t Repeat Yourself” and requires that any piece of domain knowledge has a single representation in your code base. In other words, in requires that you don’t duplicate the domain knowledge.

DRY vs. DAMP: the dichotomy

You can often hear that people put these two principles in opposition to each other. DRY and DAMP are usually represented as two ends of the spectrum that covers all of your code:

DRY and DAMP as two ends of the spectrum
  • For your production code, you should err on the side of the DRY principle
  • For the test code, you should favor DAMP over DRY
public class UserController
{
public string Register(string userName, string email)
{
User userByName = _dbContext.Users
.SingleOrDefault(x => x.UserName == userName);
if (userByName != null)
return "User with such username already exists";

User userByEmail = _dbContext.Users
.SingleOrDefault(x => x.Email == email);
if (userByEmail != null)
return "User with such email already exists";

// Register the user

return "OK";
}

public string EditPersonalInfo(string userName, string email)
{
User currentUser = GetCurrentUser();

User userByName = _dbContext.Users
.SingleOrDefault(x =>
x.UserName == userName &&
x.UserName != currentUser.UserName);
if (userByName != null)
return "User with such username already exists";

User userByEmail = _dbContext.Users
.SingleOrDefault(x =>
x.Email == email &&
x.UserName != currentUser.Email);
if (userByEmail != null)
return "User with such email already exists";

// Update personal info

return "OK";
}
}
public class UserController
{
public string Register(string userName, string email)
{
if (UserNameAlreadyExists(userName, null))
return "User with such username already exists";

if (UserEmailAlreadyExists(email, null))
return "User with such email already exists";

// Register the user

return "OK";
}

public string EditPersonalInfo(string userName, string email)
{
User currentUser = GetCurrentUser();

if (UserNameAlreadyExists(userName, currentUser.UserName))
return "User with such username already exists";

if (UserEmailAlreadyExists(email, currentUser.Email))
return "User with such email already exists";

// Update personal info

return "OK";
}

private bool UserNameAlreadyExists(
string name, string currentUserName)
{
IQueryable<User> query = _dbContext.Users
.Where(x => x.UserName == name);

if (currentUserName != null)
{
query = query.Where(x => x.UserName != currentUserName);
}

User user = query.SingleOrDefault();

return user != null;
}

private bool UserEmailAlreadyExists(
string email, string currentUserEmail)
{
/* Same for email */
}
}
[Fact]
public void Division_by_zero()
{
// Arrange
int dividend = 10;
int divisor = 0;
var calculator = new CalculatorController();

// Act
Envelope<int> response = calculator.Divide(dividend, divisor);

// Assert
response.IsError.Should().BeTrue();
response.ErrorCode.Should().Be("division.by.zero");
}

[Fact]
public void Division_of_two_numbers()
{
// Arrange
int dividend = 10;
int divisor = 2;
var calculator = new CalculatorController();

// Act
Envelope<int> response = calculator.Divide(dividend, divisor);

// Assert
response.IsError.Should().BeFalse();
response.Result.Should().Be(5);
}
/* The initialization code that is common for both tests */
int _dividend = 10;
CalculatorController _calculator = new CalculatorController();

[Fact]
public void Division_by_zero()
{
// Arrange
int divisor = 0;

// Act
Envelope<int> response = _calculator.Divide(_dividend, divisor);

// Assert
response.IsError.Should().BeTrue();
response.ErrorCode.Should().Be("division.by.zero");
}

[Fact]
public void Division_of_two_values()
{
// Arrange
int divisor = 2;

// Act
Envelope<int> response = _calculator.Divide(_dividend, divisor);

// Assert
response.IsError.Should().BeFalse();
response.Result.Should().Be(5);
}
  • It introduces high coupling between tests — If you need to modify the calculator setup in one test, it will affect the other test too.
  • It diminishes test readability — After extracting the two lines, you no longer see the full picture just by looking at individual tests. You have to examine the whole class in order to understand what the test does.

The DRY vs. DAMP dichotomy is false

So, again, the common belief is that you can’t adhere to both DRY and DAMP with the same piece of code, and the choice should be the following:

  • For the production code, prefer DRY over DAMP
  • For the test code, do the opposite choice.
public static string GetStatus(bool isLocked)
{
return (isLocked ? "L" : "Unl") + "ocked";
}
public static string GetStatus(bool isLocked)
{
return isLocked ? "Locked" : "Unlocked";
}
/* The initialization code that is common for both tests */
int _dividend = 10;
CalculatorController _calculator = new CalculatorController();

[Fact]
public void Division_by_zero()
{
// Arrange
int divisor = 0;

// Act
Envelope<int> response = _calculator.Divide(_dividend, divisor);

// Assert
response.IsError.Should().BeTrue();
response.ErrorCode.Should().Be("division.by.zero");
}

[Fact]
public void Division_of_two_values()
{
// Arrange
int divisor = 2;

// Act
Envelope<int> response = _calculator.Divide(_dividend, divisor);

// Assert
response.IsError.Should().BeFalse();
response.Result.Should().Be(5);
}

DRY and DAMP as How-to’s and What-to’s

I hope you can see now why the dichotomy between DRY and DAMP is false. You should avoid the code reuse shown in the previous example, but not because DRY somehow doesn’t apply to tests. It’s because such a reuse is an misapplication of DRY, and it is a misapplication regardless of whether you do that in tests or in the production code.

Both production and test code contain domain knowledge
  • The scenarios that need to be performed to verify the application’s correctness
  • The steps that should be in those scenarios
[Fact]
public void Division_by_zero()

[Fact]
public void Division_of_two_numbers()
int dividend = 10;
int divisor = 2;

response.Result.Should().Be(5);
  • The what-to’s answer the question of what we are testing. They describe a test scenario using specific steps relevant for that scenario.
  • The how-to’s contain the knowledge of how to implement those specific steps — how these are steps executed.
public class CalculatorController
{
private int _memorizedDividend;

public void Memorize(int dividend)
{
_memorizedDividend = dividend;
}

public Envelope<int> Divide(int divisor)
{
if (divisor == 0)
return Envelope<int>.Error(Errors.DivisionByZero);

int result = _memorizedDividend / divisor;

return Envelope<int>.Ok(result);
}
}
[Fact]
public void Division_by_zero_with_memorization()
{
int dividend = 10;
int divisor = 0;
var calculator = new CalculatorController();
calculator.Memorize(dividend); // the additional method call

Envelope<int> response = calculator.Divide(divisor);

response.IsError.Should().BeTrue();
response.ErrorCode.Should().Be("division.by.zero");
}

[Fact]
public void Division_of_two_values_with_memorization()
{
int dividend = 10;
int divisor = 2;
var calculator = new CalculatorController();
calculator.Memorize(dividend); // the additional method call

Envelope<int> response = calculator.Divide(divisor);

response.IsError.Should().BeFalse();
response.Result.Should().Be(5);
}
[Fact]
public void Division_by_zero_with_memorization()
{
int divisor = 0;
CalculatorController calculator =
CreateCalculatorWithMemorizedDividend(10);

Envelope<int> response = calculator.Divide(divisor);

response.IsError.Should().BeTrue();
response.ErrorCode.Should().Be("division.by.zero");
}

[Fact]
public void Division_of_two_values_with_memorization()
{
int divisor = 2;
CalculatorController calculator =
CreateCalculatorWithMemorizedDividend(10);

Envelope<int> response = calculator.Divide(divisor);

response.IsError.Should().BeFalse();
response.Result.Should().Be(5);
}

private CalculatorController
CreateCalculatorWithMemorizedDividend(int dividend)
{
var calculator = new CalculatorController();
calculator.Memorize(dividend);
return calculator;
}
  • We don’t need to duplicate the knowledge of how to create the calculator with the memorized dividend.
  • At the same time, we don’t compromise on the test readability. Thanks to the descriptive name of this method, we don’t need to examine the internals of this step in order to understand the attributes of the created calculator. Because we are keeping all the steps intact, we have the full context of what is going on in the tests.
public class UserController
{
public string Register(string userName, string email)
{
if (UserNameAlreadyExists(userName, null))
return "User with such username already exists";

if (UserEmailAlreadyExists(email, null))
return "User with such email already exists";

// Register the user

return "OK";
}

public string EditPersonalInfo(string userName, string email)
{
User currentUser = GetCurrentUser();

if (UserNameAlreadyExists(userName, currentUser.UserName))
return "User with such username already exists";

if (UserEmailAlreadyExists(email, currentUser.Email))
return "User with such email already exists";

// Update personal info

return "OK";
}

private bool UserNameAlreadyExists(
string name, string currentUserName)
{
/* Look in the database if the user name exists */
}

private bool UserEmailAlreadyExists(
string email, string currentUserEmail)
{
/* Look in the database if the user email exists */
}
}

Summary

  • DRY stands for “Don’t Repeat Yourself” and requires that any piece of domain knowledge has a single representation in your code base.
  • DAMP stands for “Descriptive and Meaningful Phrases” and promotes the readability of the code.
  • People often put these two principles in opposition to each other saying that:
  • The dichotomy between DRY and DAMP is false. Both principles are equally important in both the production and test code.
  • Think of DAMP and DRY in terms of what-to’s versus how-to’s:
  • What-to’s answer the question of what we are doing; they describe the use case (in the case of the production code) or a test scenario (in the case of the test code) using specific steps
  • How-to’s contain the knowledge of how to implement those specific steps
  • The DRY principle should be applied to the how-to’s, whereas the DAMP principle should be applied to the what-to’s.

Subscribe

Subscribe to read more articles like this: https://enterprisecraftsmanship.com/subscribe

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store