Is it because the production class or method are covered by other test class/method INDIRECTLY?
Basically yes. The tests are there to ensure the system has some property. Usually that the system provides some functionality. (We sometimes write test to reproduce a defective behavior, too, however, for instance when filing a bug report.) So if you write new tests that are less fragile because they are more decoupled from the production code, and also more expressive because they state what functionality the system achieves, and not how the system achieves it; the old direct tests don't provide any additional value, and should be removed because they are a maintenance burden. Indirection enables you to achieve both decoupling and expressiveness.
Are there general design pattern or rules that can help writing code that decouples test from production code?
Uncle Bob's green diagram on the right would be and example of the facade pattern because it hides multiple, complicated services behind a single interface.
To clarify one thing for junior developers do not read Uncle Bob's UML diagram literally. Services implement the facade in a loose sense.
See below example to see how application services implement the test facade. Also to see the difference/relationship between and the application facade and the test facade (as per @jonrsharpe's comment). A tip can be: write your test facade such that it reads like the stories you will demo, or the scenarios in your test plan etc. :
class SomeTestClass
{
public void users_cant_register_without_providing_contact_info()
{
failsWithMissingContactInfoErrorMessage(
() -> user.sendsRegistrationFormWithMissingContactInfo());
admin.doesntSeeInRegisteredUsers(user);
}
private failsWithMissingContactInfoErrorMessage(Runnable r)
{
failsWithErrorMentioning(r, MissingContactInfo.class, "missing contact");
}
}
class UserFacade // Can be named after roles
{
CommandInterface handler;
ViewInterface view;
String enteredEmail;
String enteredPhoneNumber;
void sendsRegistrationFormWithMissingContactInfo()
{
fillsTheForm();
leavesEmailBlank();
leavesPhoneNumberBlank();
sendsRegistrationForm();
}
void leavesEmailBlank()
{
this.enteredEmail = "";
}
void leavesPhoneNumberBlank()
{
this.enteredPhoneNumber = "";
}
void entersAValidEmail()
{
this.enteredEmail = "[email protected]";
}
void entersAValidPhoneNumber()
{
this.enteredPhoneNumber = "+1-541-754-3010";
}
void entersAnInvalidPhoneNumber()
{
this.enteredPhoneNumber = "##123456**";
}
void sendsRegistrationForm()
{
handler.handle(new RegisterNewUser(enteredEmail, enteredPhoneNumber));
}
}
class AdminFacade
{
BackEnd backEnd;
void doesntSeeInRegisteredUsers(UserFacade user)
{
// ....
}
}