Pages

Friday, July 26, 2013

If you break something you have to pay for it!

Many years ago in Spain(as a kid) I was probably in second or third year of primary school, I had to buy a gift for somebody's birthday.
All I had was 500 pesetas(a bit less than 3 euros) and I wanted to buy something cheap, but at the same time beautiful and also decorative.
I was walking around the neighbourhood and not far away from my house, I found a glass craft shop.
I went into the shop and started walking the corridors, there were all sort of glass crafts: from ashtrays to lamps... and everything was all over the place, not just in selves also there were dozens of crafts on the ground.
Quickly I noticed a little figure with a price tag that said 500, that was the gift I wanted to buy. Unfortunately It was in the ground placed at least 2 meters away from were I was, and there were lots of other glass crafts on the way to it. If I wanted to reach it all I could do was move away each of those other crafts that were on the way. I carefully started doing it until I've got the figure I wanted. When I was ready to go back and turned around the shop attendant was looking at me and said: "If you break something you have to pay for it!", I've got a bit scared and... well... is up to your imagination how this story ended.

This story reminds me a bit to the feeling that we all have sometimes when unit testing some code: "Is this really a unit test?".
Sometimes we think we tested correctly but if we are not extra careful with our unit tests we might unconsciously be creating a poisonous integration test.

Let's have a look at an example:

1:  public class ProductDiscountCalculator {  
2:       public void apply50PercentDiscount(Product product) {  
3:            ProductManager manager = new ProductManager();  
4:            if(product.isExpired()) {  
5:                 product.setPrice(product.getPrice() - product.getPrice() * 0.5);  
6:            }  
7:            manager.updateProduct(product);  
8:       }  
9:  }  

We want to unit test the method in the above class and this is what we do:

1:  public class ProductDiscountCalculatorSpecification {  
2:       private ProductDiscountCalculator calculator;  
3:       private Product sampleProduct;  
4:       @Before  
5:       public void arrange() {  
6:            calculator = new ProductDiscountCalculator();  
7:            sampleProduct = new Product();  
8:            sampleProduct.setPrice(10);  
9:            sampleProduct.setExpired(true);  
10:       }  
11:       @Test  
12:       public void priceDiscounted50Percent() {  
13:            calculator.apply50PercentDiscount(sampleProduct);  
14:            assertThat(sampleProduct.getPrice(), is(5D));  
15:       }  
16:  }  

When we run this test, it will go green, but... Is this really a unit test?
What about this line:
 manager.updateProduct(product);  

That line is very dangerous, maybe it is accessing a database and we are poisoning it every time we run our tests.
That's why we need to have extra careful and make sure that we use techniques to break dependencies, stubs, fakes, spies, mocks... or anything
that takes, to make sure at 100% that we are unit testing.
Lets see how we can break the ProductManager dependency and create a more testable alternative:

1:  public class ProductDiscountCalculator {  
2:       private ProductManager manager;  
3:       public ProductDiscountCalculator(ProductManager manager) {  
4:            this.manager = manager;  
5:       }  
6:       public void apply50PercentDiscount(Product product) {  
7:            if(product.isExpired()) {  
8:                 product.setPrice(product.getPrice() - product.getPrice() * 0.5);  
9:            }  
10:            manager.updateProduct(product);  
11:       }  
12:  }  

In the above example we extract the dependency and make sure that we can set it from outside of the class.  We could do it different ways I just used composition for this example.

1:  public class ProductDiscountCalculatorSpecification {  
2:       class ProductManagerStub extends ProductManager {  
3:            @Override  
4:            public void updateProduct(Product product) {  
5:            }  
6:       }  
7:       private ProductDiscountCalculator calculator;  
8:       private Product sampleProduct;  
9:       @Before  
10:       public void arrange() {  
11:            calculator = new ProductDiscountCalculator(new ProductManagerStub());  
12:            sampleProduct = new Product();  
13:            sampleProduct.setPrice(10);  
14:            sampleProduct.setExpired(true);  
15:       }  
16:       @Test  
17:       public void priceDiscounted50Percent() {  
18:            calculator.apply50PercentDiscount(sampleProduct);  
19:            assertThat(sampleProduct.getPrice(), is(5D));  
20:       }  
21:  }  

In what regards to the test I used the subclass and override technique to make sure that we are not poisoning a the database if the real object was passed. Since the object that we are passing to the constructor of ProductDiscountCalculator is just an stub, we are now 100% sure that this was correctly tested without poisonous integration tests.

No comments:

Post a Comment

Share with your friends