public class MySingleton {
private static final MySingleton instance = new MySingleton();
private MySingleton() { }
public int doSomething() {
//create connection to database, write to a file, etc..
return something;
}
public static MySingleton getInstance() {
return instance;
}
}
public class OtherClass {
public int myMethod() {
//do some stuff
int result = MySingleton.getInstance().doSomething();
//do some other suff
return something;
}
}
为了测试myMethod,我们必须进行实际的数据库调用、文件操作等
@Test
public void testMyMethod() {
OtherClass obj = new OtherClass();
//if this fails it might be because of some external code called by
//MySingleton.doSomething(), not necessarily the logic inside MyMethod()
Asserts.assertEqual(1, obj.myMethod());
}
如果MySingleton是这样的:
public class MyNonSingleton implements ISomeInterface {
public MyNonSingleton() {}
@Override
public int doSomething() {
//create connection to database, write to a file, etc..
return something;
}
}
然后你可以像这样将它作为一个依赖注入MyOtherClass:
public class OtherClass {
private ISomeInterface obj;
public OtherClass(ISomeInterface obj) {
this.obj = obj;
}
public int myMethod() {
//do some stuff
int result = obj.doSomething();
//do some other stuff
return something;
}
}
你可以这样测试:
@Test
public void TestMyMethod() {
OtherClass obj = new OtherClass(new MockNonSingleton());
//now our mock object can fake the database, filesystem etc. calls to isolate the testing to just the logic in myMethod()
Asserts.assertEqual(1, obj.myMethod());
}
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
public class Main {
@Test
void test(){
SpellChecker s = Mockito.mock(SpellChecker.class); //IMPOSSIBLE
when(s.check(any())).thenReturn(false);
Client c = new Client(s);
assertThat(c.check("abc")).isEqualTo(false);
}
}
class SpellChecker{
private static final SpellChecker INSTANCE = new SpellChecker();
private SpellChecker(){throw new AssertionError();}
public boolean check(String word){return true;}
public static SpellChecker getInstance(){return INSTANCE;}
}
class Client{
private SpellChecker s;
Client(SpellChecker s){this.s=s;}
boolean check(String str){return s.check(str);}
}
除非它实现了一个作为其类型的接口。
public class Main {
@Test
void test(){
SpellCheckerI s = Mockito.mock(SpellCheckerI.class); //POSSIBLE
when(s.check(any())).thenReturn(false);
Client c = new Client(s);
assertThat(c.check("abc")).isEqualTo(false);
}
}
interface SpellCheckerI{boolean check(String word);}
class SpellChecker implements SpellCheckerI{
private static final SpellChecker INSTANCE = new SpellChecker();
private SpellChecker(){throw new AssertionError();}
@Override public boolean check(String word){return true;}
public static SpellChecker getInstance(){return INSTANCE;}
}
class Client{
private SpellCheckerI s;
Client(SpellCheckerI s){this.s=s;}
boolean check(String str){return s.check(str);}
}
4条答案
按热度按时间b09cbbtk1#
如果你的单例是在数据库上执行操作或向文件中写入数据呢?您不希望在单元测试中发生这种情况。您可能希望模拟对象以在内存中执行某些操作,这样您就可以在没有永久副作用的情况下验证它们。单元测试应该是自包含的,不应该创建到数据库的连接,也不应该使用外部系统执行其他操作,这些操作可能会失败,然后导致单元测试因不相关的原因而失败。
pseudo-java示例(我是C#开发人员):
为了测试
myMethod
,我们必须进行实际的数据库调用、文件操作等如果
MySingleton
是这样的:然后你可以像这样将它作为一个依赖注入MyOtherClass:
你可以这样测试:
yhuiod9q2#
我个人认为这种说法完全是错误的,因为它假设单例对于单元测试是不可替换的(可模拟的)。恰恰相反。例如,在Spring的依赖注入中,singleton实际上是DI组件的默认模型。单例和依赖注入并不是相互排斥的,上面的陈述试图暗示这一点。
我同意任何不能被模仿的东西都会使应用程序更难测试,但是没有理由认为单例比应用程序中的任何其他对象都更难模仿。
可能的问题是,单例是一个全局示例,当它可以处于太多不同的状态时,单元测试可能会因为单例的状态变化而显示不可预测的结果。但是有一些简单的解决方案-模拟你的单例,让你的模拟有更少的状态。或者以这样一种方式编写测试,即在依赖于它的每个单元测试之前重新创建(或重新初始化)单例。或者,最好的解决方案是,针对单例的所有可能状态测试应用程序。最终,如果现实需要多个状态,例如数据库连接(断开/连接/连接/错误/...),那么无论是否使用单例,您都必须处理它。
mxg2im7a3#
无法用mock实现替代singleton
除非它实现了一个作为其类型的接口。
P.S.你可能也想看看this的精彩文章。顺便说一句,单元测试状态danger不是一个很好的例子(它不是一个单元测试),但是抓住重点更重要。
gfttwv5a4#
如果你愿意的话,你可以用一个模拟的实现来代替一个单例