How often did you hear developers who were proving quality of their application’s code by sharing information about really high coverage and a huge number of unit tests? How often did you hear how unit tests can make your life easier. How having unit tests makes it possible to introduce changes and make refactoring safier? Do you believe in it? What’s your experience?
I will show you why all of those “proofs” are invalid and wrong. I will show you the false impression of feeling safe in those dangerous places where your code is covered only with tones of unit tests.
But it won’t be all. I won’t leave you alone in this place. I will tell you what you can do to make things better.
I will show you the difference between coverage and quality. Between unit tests and safety.
2. Who am I?
Fanatic of the OOP and the Code’s Quality
Blogger
Speaker
Trainer and consultant
Software Developer at Luxoft
@SebastianMalaca
letstalkaboutjava.blogspot.com
8. Documentation? Or implementation?
void apply(ClassCode code) {
if (code.isComplex() || code.isUnreadable()) {
refactor(code);
}
}
@Test
public void shouldRefactorCodeWhenIsUnreadable() { ... }
@Test
public void shouldRefactorCodeWhenIsComplex() { ... }
@Test
public void shouldNotRefactorCodeWhenIsNotComplexNorUnreadable() { ... }
9. Documentation? Or implementation?
void apply(ClassCode code) {
if (code.isComplexOrUnreadable()) {
refactor(code);
}
}
@Test
public void shouldRefactorCodeWhenIsComplexOrUnreadable() { ... }
@Test
public void shouldNotRefactorCodeWhenIsNotComplexNorUnreadable() { ... }
10. Documentation? Or implementation?
void apply(ClassCode code) {
if (code.isComplexOrUnreadable() && code.isEditable()) {
refactor(code);
}
}
@Test
public void shouldRefactorCodeWhenIsComplexOrUnreadableAndEditable()
{ ... }
@Test
public void shouldNotRefactorCodeWhenIsComplexOrUnreadableAndNotEditable()
{ ... }
@Test
public void shouldNotRefactorCodeWhenIsNotComplexNorUnreadableAndEditable ()
{ ... }
@Test
public void shouldNotRefactorCodeWhenIsNotComplexNorUnreadableAndNotEditable ()
{ ... }
11.
12. What you have to understand to understand tests?
void apply(ClassCode code) {
if (code.isComplex() || code.isUnreadable()) {
refactor(code);
}
}
complex unreadable
yes yes
yes no
no yes
no no
13. What you have to understand to understand tests?
@Test
public void
shouldRefactorCodeWhenIsComplexAndUnreadable() {
ClassCode code = mock(ClassCode.class);
given(code.isComplex()).willReturn(true);
given(code.isUnreadable()).willReturn(true);
processor.apply(code);
// assertion
}
void apply(ClassCode code) {
if (code.isComplex() || code.isUnreadable()) {
refactor(code);
}
}
complex unreadable
yes yes
yes no
no yes
no no
14. What you have to understand to understand tests?
@Test
public void shouldRefactorCodeWhenIsComplex() {
ClassCode code = mock(ClassCode.class);
given(code.isComplex()).willReturn(true);
processor.apply(code);
// assertion
}
void apply(ClassCode code) {
if (code.isComplex() || code.isUnreadable()) {
refactor(code);
}
}
complex unreadable
yes yes
yes no
no yes
no no
15. What you have to understand to understand tests?
@Test
public void shouldRefactorCodeWhenIsUnreadable() {
ClassCode code = mock(ClassCode.class);
given(code.isUnreadable()).willReturn(true);
processor.apply(code);
// assertion
}
void apply(ClassCode code) {
if (code.isComplex() || code.isUnreadable()) {
refactor(code);
}
}
complex unreadable
yes yes
yes no
no yes
no no
16. What you have to understand to understand tests?
@Test
public void
shouldNotRefactorCodeWhenIsNotComplexNorUnreadable() {
ClassCode code = mock(ClassCode.class);
processor.apply(code);
// assertion
}
void apply(ClassCode code) {
if (code.isComplex() || code.isUnreadable()) {
refactor(code);
}
}
complex unreadable
yes yes
yes no
no yes
no no
17.
18. Too much is too much…
void refactor(ClassCode code, Developer developer) {
if (gitRepository.testsExistsFor(code.getClassName())) {
Roles roles = developer.getRoles();
if (code.mayBeModifiedBy(roles)) {
GitLogin login = developer.getGitLogin();
startRefactoringOf(code, login);
}
}
}
19. void refactor(ClassCode code, Developer developer) {
if (gitRepository.testsExistsFor(code.getClassName())) {
Roles roles = developer.getRoles();
if (code.mayBeModifiedBy(roles)) {
GitLogin login = developer.getGitLogin();
startRefactoringOf(code, login);
}
}
}
Too much is too much…
@Test
public void shouldNotRefactorWhenTestsDoesNotExist()
{ ... }
@Test
public void shouldNotRefactorWhenTestsExistsAndDeveloperCannotModifyCode()
{ ... }
@Test
public void shouldRefactorWhenTestsExistsAndDeveloperCanModifyCode()
{ ... }
20. void refactor(ClassCode code, Developer developer) {
if (gitRepository.testsExistsFor(code.getClassName())) {
Roles roles = developer.getRoles();
if (code.mayBeModifiedBy(roles)) {
GitLogin login = developer.getGitLogin();
startRefactoringOf(code, login);
}
}
}
Too much is too much…
private static final GitLogin SOME_GIT_LOGIN = mock(GitLogin.class);
private static final Roles SOME_ROLES = mock(Roles.class);
private static final String SOME_CLASS_NAME = "SomeClass";
@Mock private ClassCode code;
@Mock private Developer developer;
@Before
public void init() {
given(code.getClassName()).willReturn(SOME_CLASS_NAME);
given(developer.getRoles()).willReturn(SOME_ROLES);
given(developer.getGitLogin()).willReturn(SOME_GIT_LOGIN);
}
21.
22. Do you really feel safe?
public class ProcessingHandler {
private final List<Processor> processors;
public ProcessingHandler(List<Processor> processors) {
this.processors = processors;
}
public Output process(Input input) {
for (Processor processor : processors) {
if (processor.isApplicableFor(input)) {
return processor.process(input);
}
}
return DEFAULT_OUTPUT;
}
}
23. Do you really feel safe?
public Output process(Input input) {
for (Processor processor : processors) {
if (processor.isApplicableFor(input)) {
return processor.process(input);
}
}
return DEFAULT_OUTPUT;
}
@Test
public void shouldReturnDefaultOutputWhenApplicableProcessorNotFound()
{ ... }
@Test
public void shouldReturnOutputCreatedByApplicableProcessor()
{ ... }
24. Do you really feel safe?
public class ProcessingHandler {
private final List<Processor> processors;
public ProcessingHandler(List<Processor> processors) {
this.processors = processors;
}
// some code
}
<bean id="processingHandler"
class="com.smalaca.processor.ProcessingHandler">
<constructor-arg name="processors">
<bean class="com.smalaca.processor.ConcreteProcessor1"/>
<bean class="com.smalaca.processor.ConcreteProcessor2"/>
<bean class="com.smalaca.processor.ConcreteProcessor3"/>
</constructor-arg>
</bean>
25.
26. Assumptions may change…
public class ConcreteProcessor1 extends Processor {
public Output process(Input input) {
return anOututFor(input);
}
// some code
}
27. Assumptions may change…
public class ConcreteProcessor1 extends Processor {
public Output process(Input input) {
if (isEverythingOk(input)) {
return anOututFor(input);
}
return null;
}
// some code
}
public class ConcreteProcessor1 extends Processor {
public Output process(Input input) {
return anOututFor(input);
}
// some code
}
28. Assumptions may change…
public Output process(Input input) {
for (Processor processor : processors) {
if (processor.isApplicableFor(input)) {
return processor.process(input);
}
}
return DEFAULT_OUTPUT;
}
public class ConcreteProcessor1 extends Processor {
public Output process(Input input) {
if (isEverythingOk(input)) {
return anOututFor(input);
}
return null;
}
// some code
}
29.
30. Assumptions may change…
public Output process(Input input) {
for (Processor processor : processors) {
if (processor.isApplicableFor(input)) {
return processor.process(input);
}
}
return DEFAULT_OUTPUT;
}
public class ConcreteProcessor1 extends Processor {
public Output process(Input input) {
if (isEverythingOk(input)) {
return anOututFor(input);
}
return null;
}
// some code
}
31. Assumptions may change…
public Output process(Input input) {
if (processor.isApplicableFor(input)) {
return processor.process(input);
}
return DEFAULT_OUTPUT;
}
public class ConcreteProcessor1 extends Processor {
public Output process(Input input) {
if (isEverythingOk(input)) {
return anOututFor(input);
}
return null;
}
// some code
}
32. Assumptions may change…
@Test
public void shouldReturnDefaultOutputWhenApplicableProcessorNotFound() {
given(processor.isApplicableFor(SOME_INPUT)).willReturn(false);
// some code
}
@Test
public void shouldReturnOutputCreatedByApplicableProcessor() {
given(processor.isApplicableFor(SOME_INPUT)).willReturn(true);
given(processor.process(SOME_INPUT)).willReturn(SOME_OUTPUT);
// some code
}
public Output process(Input input) {
if (processor.isApplicableFor(input)) {
return processor.process(input);
}
return DEFAULT_OUTPUT;
}
38. TDD – be lazy in a good way
void apply(ClassCode code) {
if (code.isComplex() || code.isUnreadable()) {
refactor(code);
}
}
39. TDD – be lazy in a good way
@Test
public void shouldRefactorCodeWhenCodeShouldBeImproved() {
given(code.shouldBeImproved()).willReturn(true);
// test
}
@Test
public void shouldNotRefactorCodeWhenCodeDoesNotHaveToBeImproved() {
given(code.shouldBeImproved()).willReturn(false);
// test
}
void apply(ClassCode code) {
if (code.isComplex() || code.isUnreadable()) {
refactor(code);
}
}