在Android中编写本地单元测试时,面临的局限性之一是测试是针对没有任何代码的android.jar版本运行的。 如文档所述 ,必须模拟对Android代码的任何依赖关系。
一个简单的单元测试的快速示例:
public class ClassUnderTest {
public String methodUnderTest(String str)
{
if (PhoneNumberUtils.isGlobalPhoneNumber(str))
{
return "yes";
}
else
{
return "no";
}
}
}
@RunWith(JUnit4.class)
public class TestThatFails {
private ClassUnderTest classUnderTest;
@Before
public void setup() {
classUnderTest = new ClassUnderTest();
}
@Test
public void testTheClass() {
String result = classUnderTest.methodUnderTest("1234");
assertEquals("yes", result);
}
}
运行此测试时,它将失败并显示以下错误:
java.lang.RuntimeException: Method isGlobalPhoneNumber in android.telephony.PhoneNumberUtils not mocked. See http://g.co/androidstudio/not-mocked for details
我们正在测试的类依赖于Android实用程序库PhoneNumberUtils 。 为了使测试成功运行,必须模拟此Android依赖项。
这篇文章的所有示例代码都可以在gist上找到 。
Mockito:否定静态方法
Google建议的模拟Android依赖关系的方法是使用Mockito 。 通常这很好,但是在我们的示例中,由于Mockito不支持模拟静态方法,因此将无法正常工作。
讨论表明,出于各种原因,Mockito贡献者将静态方法视为反模式,例如
- 在代码中,对静态方法的依赖变得很硬。
- 这使得模拟和测试变得困难。
因此,他们不支持它,因为他们不想鼓励不良的设计。
那么还有什么其他方法可以使我们的测试工作呢?
- 如果这是普通的Java而不是Android,那么我可以使用PowerMockito模拟静态方法。 但是我发现在Android中使用PowerMock存在问题。
- 如果仅使用一些静态方法,则可以在假定源可用的情况下将代码复制到应用程序中。 当然,这意味着需要维护更多的代码,并且如果您使用许多静态方法,这将是不可持续的。
- 您可以包装静态方法调用并在内部委托给静态方法。 然后可以嘲笑包装器。 这是我们将要讨论的选项。
包装类
一种解决方案是为具有静态方法的Android类创建包装器类,并将该包装器添加为依赖项。
public class PhoneNumberUtilsWrapper {
public boolean isGlobalPhoneNumber(String phoneNumber)
{
return PhoneNumberUtils.isGlobalPhoneNumber(phoneNumber);
}
}
public class ClassUnderTestWithWrapper {
private PhoneNumberUtilsWrapper wrapper;
public ClassUnderTestWithWrapper(PhoneNumberUtilsWrapper wrapper) {
this.wrapper = wrapper;
}
public String methodUnderTest(String str)
{
if (wrapper.isGlobalPhoneNumber(str))
{
return "yes";
}
else
{
return "no";
}
}
}
在这里,我为PhoneNumberUtils创建了一个包装类,该包装类现在是被测试类的依赖项。
@RunWith(JUnit4.class)
public class TestWithWrapper {
@Mock
PhoneNumberUtilsWrapper wrapper;
private ClassUnderTestWithWrapper classUnderTest;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
classUnderTest = new ClassUnderTestWithWrapper(wrapper);
}
@Test
public void testTheClass() {
when(wrapper.isGlobalPhoneNumber(anyString())).thenReturn(true);
String result = classUnderTest.methodUnderTest("1234");
assertEquals("yes", result);
}
}
因为可以模拟包装器类,并且被测类中的方法调用不是静态的,所以现在可以通过测试。
这种解决方案的一个问题是,当被测类依赖于许多Android库中的静态方法时。 例如,如果被测类也需要使用TextUtils , DateUtils等,会发生什么。突然之间,您最终会得到更多的样板代码,更多的构造函数参数等。
包装方法
另一种方法是将静态方法调用包装在被测类中的非静态方法中。
public class ClassUnderTestWithWrappedMethod {
public String methodUnderTest(String str)
{
if (isGlobalPhoneNumber(str))
{
return "yes";
}
else
{
return "no";
}
}
// can't be private access
boolean isGlobalPhoneNumber(String phoneNumber)
{
return PhoneNumberUtils.isGlobalPhoneNumber(phoneNumber);
}
}
为了使它起作用,在测试中我们必须使用Mockito spy 。 还要注意,包装方法必须在测试中可以访问,因此不能是私有的。
@RunWith(JUnit4.class)
public class TestWithWrappedMethod {
private ClassUnderTestWithWrappedMethod classUnderTest;
private ClassUnderTestWithWrappedMethod classUnderTestSpy;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
classUnderTest = new ClassUnderTestWithWrappedMethod();
classUnderTestSpy = Mockito.spy(classUnderTest);
}
@Test
public void testTheClass() {
doReturn(true).when(classUnderTestSpy)
.isGlobalPhoneNumber(anyString());
String result = classUnderTestSpy.methodUnderTest("1234");
assertEquals("yes", result);
}
}
在这里,我们在spy类上运行测试,它将把方法调用委派给被测真实类。 但是,我们可以为将静态方法调用包装到Android库的方法创建存根。
正如我已经提到的,一个缺点是包装方法不能是私有的,从OO设计的角度来看,这是不理想的。 但是,如果使用诸如Dagger或Butterknife之类的库,则必须做出类似的折衷。
结论
这两种包装解决方案都可以使用,但保持一致并尽可能坚持一种方式可能是个好主意。 哪种方法效果更好,可能取决于您的应用程序架构,例如您是否在使用依赖项注入。
静态方法:好还是坏? 有关系吗?
在本文中,我不会讨论静态方法是好还是坏的设计(尽管我个人认为应该限制使用它们)。 互联网上已经有很多关于此问题的争论。
但是,在Java世界中,它们已成为现实。
Java,Android和许多流行的库中的许多实用程序类不是真正的OO类,而是过程函数的集合。 这些功能通常被编写为静态方法,并按功能分组。
无论您是否喜欢静态方法,我们都必须学会以务实的方式处理它们。
翻译自: https://www.javacodegeeks.com/2017/12/mocking-static-methods-android-lets-wrap.html