Performance Zone is brought to you in partnership with:

I'm a Lead engineer at Terracotta, Software AG. I have 10+ years of experience with Java, but I also speak other languages, C, C++, Javascript, Coffeescript, PHP, Perl... See my blog to know about my different personal projects! Aurelien is a DZone MVB and is not an employee of DZone and has posted 17 posts at DZone. You can read more from them at their website. View Full User Profile

Plan a Scalable Architecture Through Fault Injection

05.25.2012
| 4173 views |
  • submit to reddit

I have been doing a lot of testing on scalable architectures since last year and at some point, Steve Harris pointed out the principles of fault injection in a discussion (Thanks Steve for making me think about this).

When I started to look at it more closely, I realized a couple of things:

- Unit testing is good, there are some great principles, but sometimes they are not explained clearly enough
- Testing an architecture for scaling is a different piece of work. You need way more than unit testing.

I didn’t really understand unit testing
You surely have heard that when writing unit tests, you should think of testing failing cases, not working ones.
Then you started writing tests and didn’t really think of a good failing case so you wrote a test for a case that works.
Then after some thinking you found a failing case (usually putting a NPE somewhere).
Then you got used to that and when writing unit tests, you cover both working and failing cases.

I used to do that, and I changed and now I see I write better applications.

Let me explain.

It all started when I wanted to add fault injections in my tests.

What is fault injection?

From Wikipedia:
“In software testing, fault injection is a technique for improving the coverage of a test by introducing faults to test code paths”

Put it more clearly, let’s take an example:

public class TextApp {

  public static void main(String[] arg) {
    try {
      String filename = "someFile.txt";
      String content = "Let's write this in the file";
      WriterService writerService = new WriterServiceImpl();
      writerService.writeOnDisk(filename, content);
    } catch (Exception e) {
      System.out.println("We couldn't write on the disk. " + e.getMessage());
    }
  }
}
import java.io.IOException;

public interface WriterService {
  void writeOnDisk(String filename, String content) throws Exception;
}
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

public class WriterServiceImpl implements WriterService {

  public void writeOnDisk(final String filename, final String content) throws Exception {
    try {
      FileWriter file = new FileWriter(filename);
      BufferedWriter out = new BufferedWriter(file);
      out.write(content);
      out.close();
    } catch (IOException e) {
      throw new Exception("file writing error", e);
    }
  }
}

And I want to write a unit test for the WriterService:

import org.junit.Assert;
import org.junit.Test;

public class WriterServiceTest {

  @Test
  public void testSuccessfulWriting() {
    WriterService writerService = new WriterServiceImpl();
    try {
      writerService.writeOnDisk("test.txt", "testContent");
    } catch (Exception e) {
      Assert.fail("We should be able to write to the disk");
    }
  }

  @Test
  public void testFailingWriting() {
    WriterService writerService = new WriterServiceImpl();
    try {
      writerService.writeOnDisk("impossiblefilename^&:/.txt", null);
      Assert.fail("We should not be able to write to the disk");
    } catch (Exception e) {
      Assert.assertEquals("file writing error", e.getMessage());
    }
  }
}

And I’ve done exactly what I described : Write a unit test with a working case and a failing case.

Let’s say I’m satisfied enough (obviously the code here is minimal and should be improved but for the demonstration, we will say that the code in the WriterService is satisfying).

Now I could improve the coverage of my test through fault-injection.
So I decide to inject errors at runtime in the service while executing my unit test.

For instance, I can think of the case where the disk is full, I want to test that.

But how can I write a test that will execute when the disk is full? Well, I am going to simulate(inject) that fault.

There is a pretty good API for that called Byteman, which is a bytecode manipulation api, with a JUnit/TestNG runner, so basically, when using this library you could then write a test like this:

...
 @BMRule(name="throw sync failed exception",
    targetClass="FileInputStream",
    targetMethod="(File)",
    condition="$1.getName().equals\"diskfullname.txt\")"
    action="throw new SyncFailedException(\"can't sync to disk!\")")
  @Test
  public void testDiskfull() {
   WriterService writerService = new WriterServiceImpl();
    try {
      writerService.writeOnDisk("diskfullname.txt", null);
      Assert.fail("We should not be able to write to the disk");
    } catch (Exception e) {
      Assert.assertEquals("file writing error", e.getMessage());
    }
  }
...

And that’s very nice, I can write more tests, have more robust code.

But I can do better.

Let’s forget about Byteman for a second, how would you do if you have to do it by yourself?

I have an idea.

I am going to break the WriterService into two classes : WriterService and DiskWriterService

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

public class WriterServiceImpl implements WriterService {

  DiskWriterService diskWriterService = new DiskWriterServiceimpl();
  
  public void writeOnDisk(final String filename, final String content) throws Exception {
    try {
      diskWriterService.write(filename, content);
    } catch (IOException e) {
      throw new Exception("file writing error", e);
    }
  }

  public void setDiskWriterService(final DiskWriterService diskWriterService) {
    this.diskWriterService = diskWriterService;
  }
}
import java.io.IOException;

public interface DiskWriterService {
  void write(String filename, String content) throws IOException;
}

 

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

public class DiskWriterServiceimpl implements DiskWriterService {
  public void write(final String filename, final String content) throws IOException {
    FileWriter file = new FileWriter(filename);
    BufferedWriter out = new BufferedWriter(file);
    out.write(content);
    out.close();
  }
}

Breaking the service in two adds an essential advantage to my application: It makes it more loosely coupled. I probably don’t need to explain in details what is loose coupling, basically it’s the idea of breaking an app into components that do not need to know the state of other components to work.

With this I can write the following class:

import java.io.IOException;

public class FaultyDiskWriterServiceImpl implements DiskWriterService {
  
  private IOException exception;
  
  public void write(final String filename, final String content) throws IOException {
    throw exception;
  }

  public void setException(final IOException exception) {
    this.exception = exception;
  }
}

and then I can replace my test case by this:

@Test
  public void testDiskfull() {
    WriterServiceImpl writerService = new WriterServiceImpl();
    FaultyDiskWriterServiceImpl faultyDiskWriterService = new FaultyDiskWriterServiceImpl();
    faultyDiskWriterService.setException(new SyncFailedException("disk full"));
    writerService.setDiskWriterService(faultyDiskWriterService);
    try {
      writerService.writeOnDisk("diskfullname.txt", null);
      Assert.fail("We should not be able to write to the disk");
    } catch (Exception e) {
      Assert.assertEquals("file writing error", e.getMessage());
    }
  }

So let’s think for a minute, what did I do?

– I wrote a failing test case (disk full)

– It forced me to break my WriterService in two, which is good because it makes my application more loosely coupled. If I have another service that needs to write on disk, I could reuse that DiskWriterService

- I can now unit test my DiskService and make sure that writing on the disk is robust.

It separates the testing of writing on the disk and the testing of my WriterService (now it is just a simple case where it calls the DiskWriterService but it could do more things like logging, writing to multiple places…).

– With this new DiskWriterService, it implicitly makes my architecture more robust for scalability, if someday I need to write to more than one disk or different kind of storage, (CloudDiskWriter, QueueDiskWriter…), I could write other DiskWriterService implementation without touching the rest of the code.

So now I see more clearly the point of testing failing cases. In case you have no idea when doing such thing, my advice would be “Think of the errors you could inject in your code when writing unit tests”

That’s it for point 1, next time for point 2 – Testing for scaling!

Published at DZone with permission of Aurelien Broszniowski, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Tags:

Comments

Herry Johnson replied on Tue, 2012/06/12 - 12:55pm

But if you would have looked at the javadoc of that method you would have very quickly known this was not the case and you wouldn't even had to create this thread. Why operate on guesswork when it is so easy to know reality? :(

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.