简体   繁体   中英

Android Unit Testing : How do I test this?

I'm testing out an android app, and am using a library provided to me by my university, classes 1-4 come from my lecturer for our use.

I have a class structured like so:

ClassOne

public ClassOne {
    private ClassTwo clsTwo;
    ...
    public ClassOne(ClassTwo p1)
    public ClassTwo getClsTwo();
}

ClassTwo is structured as so:

public ClassTwo {
    private ClassThree clsThree;
    ...
    public ClassTwo()
    public ClassThree getClsThree();
}

ClassThree is structured as so:

public ClassThree {
    private HashMap<Bitmap> mBitmaps;
    ...
    private ClassFour clsFour;
    ...
    public ClassThree(ClassFour p1);
    ...
    public loadFile(String path, String name);
    public loadFileFromAssetStore(String name);
}

ClassFour is structured as so:

public ClassFour {
    ...
    public ClassFour(Context context);
    ...
}

The Class I am testing is ClassFive, which specifically has the methods highlighted which are causing issues:

public ClassFive {
   private Bitmap myBitmap
   ... 
   public ClassFive(...,...,...,ClassOne p,...){
       super(..., p, 
             p.getClsTwo().getClsThree().loadFileFromAssetStore("Default value"));
        this.myBitmap = loadCorrectFile(...,p);
   }
   private Bitmap loadCorrectFile(..., ClassOne p){
       String strCorrectFileName;
       switch(...){
          ...
          // set value of strCorrectFileName
          ...
       }
      this.myBitmap = p.getClsTwo().getClsThree().loadFileFromAssetStore(strCorrectFileName);
   }
}

My problem is I need to test methods using constructor of ClassFive, however the tests are all 'falling over' when invoking the constructor with a NPE.

public class ClassFiveTest {

@Mock
private ClassOne mockClassOne = Mockito.Mock(ClassOne.class);

@Test
public void testConstructorGetName() throws Exception {
    ClassFive instance = new ClassFive(..., mockClassOne);
    ...
    // Assertions here 
    ...
}

My problem is that a null pointer exception is being returned before my test can get to my assertions. Do I need to be using mockito? Because I tried that - maybe I'm just using it wrong for this instance. Or do I need to be using instrumented tests? When I tried instrumented testing I found it impossible to get access to ClassOne and ClassTwo?

This is easily remedied with some stubbing .

@Mock private ClassOne mockClassOne; // Don't call `mock`; let @Mock handle it.
@Mock private ClassTwo mockClassTwo;
@Mock private ClassThree mockClassThree;

@Override public void setUp() {
  MockitoAnnotations.initMocks(this); // Inits fields having @Mock, @Spy, and @Captor.
  when(mockClassOne.getClsTwo()).thenReturn(mockClassTwo);
  when(mockClassTwo.getClsThree()).thenReturn(mockClassThree);

  // Now that you can get to mockClassThree, you can stub that too.
  when(mockClassThree.loadFileFromAssetStore("Default value")).thenReturn(...);
  when(mockClassThree.loadFileFromAssetStore("Your expected filename")).thenReturn(...);
}

In summary, Mockito is designed for easily making replacement instances of classes so you can check your interactions with your class-under-test: Here, you're creating fake ("test double") implementations of ClassOne, ClassTwo, and ClassThree, for the purpose of testing ClassFive. (You might also choose to use real implementations or manually-written fake implementations, if either of those make more sense for your specific case than Mockito-produced implementations.) Unless you otherwise stub them, Mockito implementations return dummy values like zero or null for all implemented methods, so trying to call getClsThree on the null returned by getClsTwo causes an NPE until you stub getClsTwo otherwise.

If the stubs for mockThree change between tests, you can move them into your test before you initialize your ClassFive. I'm also sticking to JUnit3 syntax and explicit initMocks above, because Android instrumentation tests are stuck on JUnit3 syntax if you're not using the Android Testing Support Library ; for tests on JUnit4 or with that library you can use a cleaner alternative to initMocks . Once you get comfortable with Mockito, you can also consider RETURNS_DEEP_STUBS , but I like to keep my stubs explicit myself; that documentation also rightly warns "every time a mock returns a mock, a fairy dies".


Isn't this long and complicated, and doesn't it feel unnecessary? Yes. You are working around violations of the Law of Demeter , which Wikipedia summarizes (emphasis mine) as:

  • Each unit should have only limited knowledge about other units: only units "closely" related to the current unit.
  • Each unit should only talk to its friends; don't talk to strangers.
  • Only talk to your immediate friends.

Your problem and your verbose solution both stem from ClassFive depending on ClassThree, but only via ClassOne and ClassTwo implementation details. This isn't a strict law, but in your own code outside of university you might treat this as a sign to revisit the designs of ClassOne, ClassTwo, and ClassFive and how they interact. If ClassFive were to depend directly on ClassThree, it may be easier to work with the code in production and tests, and maybe you'd find that ClassOne isn't necessary at all.

// ClassFive doesn't just work with its dependency ClassOne, it works directly with its
// dependency's dependency's dependency ClassThree.
super(..., p, 
    p.getClsTwo().getClsThree().loadFileFromAssetStore("Default value"));

I'd like to support the answer of@JeffBowman by showing how the code could look like.

The proposed solution implies that you add another parameter to the constructors parameter list with is far to long already. Your code could be simplified by following the Favor composition over inheritance principle

Most parameters of the constructor in ClassFive are only there to be pass to the parent classes constructor.

In this situation it would be better not to inherit from that super class, but create an interface (eg: extract with support of your IDE) of the super class (lets call is SuperInterface that is implemented by both, the super class and CLassFive .

The you replace all the parameters that are passed to the super class by one single parameter of type SuperInterface .

Then you simply delegate all methods of SuperInterface that are not implemented by CLassFive directly to the SuperInterface instance.

This is what it would look like:

  public interface SuperInterface {
    // all public methods of the super class.
  }

 public class ClassFive implements SuperInterface{
    private final SuperInterface superClass;
    private final Bitmap myBitmap
    public ClassFive(SuperInterface superClass ,ClassTree p){
       this.superClass  =  superClass;
       p.loadFileFromAssetStore("Default value"));
       this.myBitmap = loadCorrectFile(...,p);
    }
    @Override
    public void someMethodDeclaredInInterface(){
       this.superClass.someMethodDeclaredInInterface();
    }
 }

This pattern also works vice versa if you don't like the duplicated method delegations all over your classes extending SuperInterface .

This alternative approach is useful if your specializations override just a few methods of the interface and almost all the same.

In that case the interface you create may not be implemented by the super class. The methods declared in the interface don't even need to be part of the super classes public methods. The interface only declares methods that the super class (should now better be called "generic class") needs to use the derived behavior.

This would look like this:

interface AnimalSound{
  String get();
}

class DogSound implements AnimalSound{
  @Override
  public String get(){
    return "wouff";
  }
}

class CatSound implements AnimalSound{
  @Override
  public String get(){
    return "meaw";
  }
}

class Animal {
   private final AnimalSound sound;
   public Animal(AnimalSound sound){
     this.sound  =  sound;
   }

   public String giveSound(){
     return sound.get();
   }
}

And this is how we use it:

List<Animal> animals = new ArrayList<>();
animals.add(new Animal(new DogSound()));
animals.add(new Animal(new CatSound()));
for(Animal animal : animals){
  System.out.println(animal.giveSound());
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM