简体   繁体   English

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

[英]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. 我正在测试一个android应用程序,并且正在使用我的大学提供给我的库,第1-4课来自我的讲师供我们使用。

I have a class structured like so: 我有一个这样的类结构:

ClassOne ClassOne

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

ClassTwo is structured as so: ClassTwo的结构如下:

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

ClassThree is structured as so: 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 is structured as so: ClassFour的结构如下:

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

The Class I am testing is ClassFive, which specifically has the methods highlighted which are causing issues: 我正在测试的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);
   }
}

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. 我的问题是我需要使用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 
    ...
}

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? 当我尝试仪器测试时,发现无法访问ClassOne和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. 总而言之,Mockito旨在轻松制作类的替换实例,以便您可以检查与被测类的交互:在这里,您正在为ClassOne,ClassTwo和ClassThree创建虚假的(“双重测试”)实现。测试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. (如果您的具体情况比Mockito产生的实现更有意义,则您也可以选择使用真实的实现或手动编写的伪实现。)除非您对它们进行存根处理,否则Mockito的实现会返回零或null类的虚拟值作为所有实现的方法,因此尝试在getClsThree返回的null上调用getClsTwo会导致NPE,直到您对getClsTwo存根。

If the stubs for mockThree change between tests, you can move them into your test before you initialize your ClassFive. 如果模拟三的存根在测试之间发生变化,则可以在初始化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 ; 我还坚持上面的JUnit3语法和显式的initMocks ,因为如果您不使用Android测试支持库 ,则Android工具测试将停留在JUnit3语法上; for tests on JUnit4 or with that library you can use a cleaner alternative to initMocks . 对于在JUnit4或该库上进行的测试,您可以使用更干净的替代initMocks Once you get comfortable with Mockito, you can also consider RETURNS_DEEP_STUBS , but I like to keep my stubs explicit myself; 一旦您对Mockito感到满意,也可以考虑使用RETURNS_DEEP_STUBS ,但是我想让自己的存根保持明确。 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: 您正在解决违反Demeter法则的问题 ,Wikipedia将该总结为:

  • 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. 您的问题和详细的解决方案均来自ClassFive,具体取决于ClassThree,但只能通过ClassOne和ClassTwo实现细节进行。 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. 这不是严格的法律,但是在大学以外的您自己的代码中,您可能会将此视为重新访问ClassOne,ClassTwo和ClassFive的设计以及它们如何交互的标志。 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直接依赖于ClassThree,那么在生产和测试中使用代码可能会更容易,也许您会发现根本不需要ClassOne。

// 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. 我想通过显示代码的样子来支持@JeffBowman的答案。

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. ClassFive中构造函数的大多数参数只能传递给父类构造函数。

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 . 在这种情况下,这将是最好不要从超类继承,而是创建一个接口(例如:提取与支持你的IDE)的超类(允许调用SuperInterface由两个实现,超类和CLassFive

The you replace all the parameters that are passed to the super class by one single parameter of type SuperInterface . 您将用SuperInterface类型的单个参数替换传递给超类的所有参数。

Then you simply delegate all methods of SuperInterface that are not implemented by CLassFive directly to the SuperInterface instance. 然后你直接委托的所有方法SuperInterface不受实施CLassFive直接向SuperInterface实例。

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 . 如果您不喜欢在扩展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());
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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