Android单元测试:我该如何测试?

时间:2016-12-31 17:46:57

标签: java android unit-testing mockito

我正在测试一个Android应用程序,并且正在使用我大学提供给我的图书馆,1-4班来自我的讲师供我们使用。

我的课程结构如下:

ClassOne

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

ClassTwo的结构如下:

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

ClassThree的结构如下:

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

ClassFour的结构如下:

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

我正在测试的Class是ClassFive,它特别强调了导致问题的方法:

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);
   }
}

我的问题是我需要使用ClassFive的构造函数来测试方法,但是当使用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 
    ...
}

我的问题是在我的测试可以到达我的断言之前返回空指针异常。我需要使用mockito吗?因为我试过了 - 也许我只是在这个例子中使用它错了。或者我是否需要使用仪器化测试?当我尝试检测测试时,我发现无法访问ClassOne和ClassTwo?

2 个答案:

答案 0 :(得分:3)

一些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(...);
}

总之,Mockito旨在轻松制作课程的替换实例,以便您可以检查您与被测试课程的互动情况:在这里,您正在创建假(&#34;测试双&#34;) ClassOne,ClassTwo和ClassThree的实现,用于测试ClassFive。 (您可能还会选择使用真实实现或手动编写的虚假实现,如果其中任何一个对于您的特定情况比Mockito生成的实现更有意义。)除非您以其他方式存根,否则Mockito实现返回虚拟值,如零或{{ 1}}对于所有已实现的方法,因此尝试在null返回的getClsThree上调用null会导致NPE,直到您以getClsTwo为其他方式。

如果mockThree的存根在测试之间发生变化,您可以在初始化ClassFive之前将它们移动到测试中。我还坚持上面的JUnit3语法和显式getClsTwo,因为如果您不使用Android Testing Support Library,Android检测测试会停留在JUnit3语法上。对于JUnit4或该库的测试,您可以使用a cleaner alternative to initMocks。一旦你对Mockito感到满意,你也可以考虑RETURNS_DEEP_STUBS,但我喜欢自己保持我的存根;文档也正确警告&#34;每当模拟返回模拟时,仙女就死了#34;

这种漫长而复杂,并不觉得没必要吗? 是。您正在解决违反Law of Demeter的问题,维基百科总结(强调我的):

  
      
  • 每个单位应该对其他单位的知识有限:只有单位&#34;密切关注&#34;与当前单位有关。
  •   
  • 每个单位只应与其朋友交谈;不要和陌生人说话。
  •   
  • 只与您的直接朋友交谈。
  •   

您的问题和详细解决方案都源于ClassFive,具体取决于ClassThree,但仅限于ClassOne和ClassTwo实现细节。这不是一个严格的法律,但在大学以外的自己的代码中,您可能会将此视为重新审视ClassOne,ClassTwo和ClassFive的设计以及它们如何交互的标志。如果ClassFive直接依赖于ClassThree,那么在生产和测试中使用代码可能会更容易,并且您可能发现ClassOne根本不是必需的。

initMocks

答案 1 :(得分:1)

我想通过展示代码的外观来支持@ JeffBowman的答案。

建议的解决方案意味着您将另一个参数添加到构造函数参数列表中,并且已经很长。通过遵循支持合成而不是继承原则

,可以简化您的代码

ClassFive中构造函数的大多数参数只能传递给父类构造函数。

在这种情况下,最好不要从超类继承,而是创建超类的接口(例如:支持IDE的提取)(允许调用SuperInterface由两者实现,超级班和CLassFive

您将一个SuperInterface类型的参数替换为传递给超类的所有参数。

然后,您只需将SuperInterface未实现的CLassFive的所有方法直接委托给SuperInterface实例。

这就是它的样子:

  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();
    }
 }

如果您不喜欢扩展SuperInterface的所有类的重复方法委派,则此模式也可以反过来。

如果您的专业化只覆盖界面的几个方法并且几乎完全相同,那么这种替代方法很有用。

在这种情况下,您创建的接口可能无法由超类实现。在接口中声明的方法甚至不需要是超类公共方法的一部分。接口只声明超类(现在应该更好地称为“泛型类”)需要使用派生行为的方法。

这看起来像这样:

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();
   }
}

这就是我们使用它的方式:

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());
}