004: Redundant Mirror Image
The “Redundant Mirror Image” code smell involves code duplication within your Gherkin scenarios. When the same implementation logic appears multiple times with different expressions, consider consolidating it into reusable expressions. Even if the expressions seem completely mismatched (representing opposite behaviors, such as success and failure cases), both scenarios verify the same behavior.
Impact
Test Inconsistency
If one side of the mirror image is updated without considering the other, scenarios may become inconsistent.
Harder Maintenance
Any change to one step affects all implementations.
Required Action
Identify steps that share the same implementation but describe asymmetric behavior. If you find such steps, consider consolidating them by using multiple expressions for a single implementation.
Code Examples
@When("the customer is created") // (1)!
public void theCustomerIsCreated() {
try {
customerService.addCustomer(firstName, lastName, DEFAULT_BIRTHDAY);
} catch (IllegalArgumentException e) {
error = e;
}
}
@When("an invalid customer is created")
public void anInvalidCustomerIsCreated() {
try {
customerService.addCustomer(firstName, lastName, DEFAULT_BIRTHDAY);
} catch (IllegalArgumentException e) {
error = e;
}
}
anInvalidCustomerIsCreatedcontains the same implementation astheCustomerIsCreated. It is the logical opposite, but does the same thing, just that we expect it to throw anIllegalArgumentExceptionin one case and haveerrorbenullin the other. The only real difference comes from the subsequentThenstep that makes a distinction in the verification.
when(u'the customer is created') # (1)!
def step_impl(context):
try:
context.service.add_customer(context.first_name, context.last_name, context.default_birthday)
except ValueError as e:
context.error = e
@when(u'an invalid customer is created')
def step_impl(context):
try:
context.service.add_customer(context.first_name, context.last_name, context.default_birthday)
except ValueError as e:
context.error = e
the customer is createdcontains the same implementation asan invalid customer is created. It is the logical opposite, but does the same thing, just that we expect it to throw aValueErrorin one case and havecontext.errorbeNonein the other. The only real difference comes from the subsequentThenstep that makes a distinction in the verification.
[When("the customer is created")]
public void WhenTheCustomerIsCreated() // (1)!
{
try
{
_customerService.AddCustomer(_firstName, _lastName, DefaultBirthday);
}
catch (ArgumentException ex)
{
_error = ex;
}
}
[When("an invalid customer is created")]
public void WhenAnInvalidCustomerIsCreated()
{
try
{
_customerService.AddCustomer(_firstName, _lastName, DefaultBirthday);
}
catch (ArgumentException ex)
{
_error = ex;
}
}
WhenAnInvalidCustomerIsCreatedcontains the same implementation asWhenTheCustomerIsCreated. It is the logical opposite, but does the same thing, just that we expect it to throw aArgumentExceptionin one case and have_errorbeNonein the other. The only real difference comes from the subsequentThenstep that makes a distinction in the verification.
func (t *CustomerTestSteps) theCustomerCreationShouldBeSuccessful(ctx context.Context) error {
// ...
sc.When(`^the customer is created$`, t.theCustomerIsCreated)
sc.When(`an invalid customer is created`, t.anInvalidCustomerIsCreated) // (1)!
// ...
}
func (t *CustomerTestSteps) anInvalidCustomerIsCreated(ctx context.Context) error {
t.err = t.customerService.AddCustomer(t.firstName, t.lastName, DEFAULT_BIRTHDAY)
return nil
}
func (t *CustomerTestSteps) theCustomerIsCreated(ctx context.Context) error {
t.err = t.customerService.AddCustomer(t.firstName, t.lastName, DEFAULT_BIRTHDAY)
return nil
}
anInvalidCustomerIsCreatedcontains the same implementation astheCustomerIsCreated. It is the logical opposite, but does the same thing, just that we expect it to set an error instead of returnungnilfor an error. The only real difference comes from the subsequentThenstep that makes a distinction in the verification.