单元测试
- 注:基于小创分享的总结
明确概念
- 单元测试 or 集成测试:
- 单元测试在维基百科是这样定义的
单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
集成测试在维基百科的定义
集成测试,整合测试又称组装测试,即对程序模块采用一次性或增殖方式组装起来,对系统的接口进行正确性检验的测试工作。整合测试一般在单元测试之后、系统测试之前进行。实践表明,有时模块虽然可以单独工作,但是并不能保证组装起来也可以同时工作
有没有理解?那我们在借鉴下《单元测试的艺术》对单元测试和集成测试的定义:
一个单元测试时一段自动化的代码,这段代码调用被测试的工作单元,之后对这个单元的单个最终结果的某些假设进行检验。单元测试几乎都是用单元测试框架编写。单元测试容易编写,能快速运行。单元测试可靠,可读,并且可维护。只要产品代码不发生变化,单元测试的结果是稳定的。
集成测试是对一个工作单元进行的测试,这个测试对被测试的工作单元没有完全控制,并使用该单元的一个或者多个真实依赖物,例如时间,网络,数据库,线程或随机数产生器等。
现在应该了解吧,没有了解不要紧,我们再通过他们关系比对
那单元测试和集成测试的关系是怎么样了?
链接
优点
保证代码质量,防止bug或尽早发现bug的作用
改善代码的设计,节约开发调试时间
大大减少重构中手动验证正确性的时间。
在写单元测试的时候也能发现方法乃至系统结构设计的不合理
前面都是官方文字,现在讲点实在的。单元测试就是测试方法,方法我们又分为两种
- 有返回值方法
- 无返回值方法
有返回值方法,我们测试这个方法直接通过测试这个方法返回的值是否跟你预期一样。我们就以一个登陆的demo来解析吧
环境
- IDE:Android Studio
- Java测试库:junit,在build.gradle配置
testCompile 'junit:junit:4.12'
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}
这个是新近一个Android新建的项目,生成的单元测试,直接通过点击方法的运行就能看的结果了。
实际开发中我们也是通过get()获取指定值比较这个应该比较容易理解。重点就是第二个了
- 无返回值方法,我们只能通过方法内某个对象的方法是否被调用并且调用参数一样。这里就要用到mock,mock其实就是虚拟一个对象,然后根据这个对象方法判断是否被调用,mock目前比较成熟的框架是Mockito,所有我们这里要引用Mockito框架
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2',
{
exclude group: 'com.android.support', module: 'support-annotations' })
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support.constraint:constraint-layout:1.0.0-alpha4'
//以下测试框架
testCompile "junit:junit:4.12"
//mockito
testCompile "org.mockito:mockito-core:1.+"}
先看代码这个是一个简单的login代码
Activity
public class MainActivity extends AppCompatActivity implements View.OnClickListener, ILoginView {
private EditText mUserNameView;
private EditText mPassWordView;
private LoginPresenter mPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.login).setOnClickListener(this);
mUserNameView = (EditText) findViewById(R.id.username);
mPassWordView = (EditText) findViewById(R.id.password);
mPresenter = new LoginPresenter(this, HttpClient.getInstance());
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.login:
String username = mUserNameView.getText().toString();
String password = mPassWordView.getText().toString();
if (TextUtils.isEmpty(username)) {
Toast.makeText(this, "用户名不能为空", Toast.LENGTH_LONG).show();
return;
} else if (TextUtils.isEmpty(password)) {
Toast.makeText(this, "密码不能为空", Toast.LENGTH_LONG).show();
return;
}
mPresenter.login(username, password);
break;
}
}
@Override
public void startHomeView() {
Toast.makeText(this, "跳转主页", Toast.LENGTH_LONG).show();
}
@Override
public void onError() {
Toast.makeText(this, "密码错误", Toast.LENGTH_LONG).show();
}
}
ILoginView接口
public interface ILoginView {
/**
* 跳转主页
*/
void startHomeView();
/**
* 错误
*/
void onError();
}
HttpClient
public class HttpClient {
public static HttpClient mClient;
public static HttpClient getInstance() {
if (mClient == null) {
synchronized (HttpClient.class) {
if (mClient == null) {
mClient = new HttpClient();
}
}
}
return mClient;
}
public interface Callback {
void onSuccess(String phone);
void onError(String error);
}
public void login(String username, String password, Callback callback) {
//为了简单,没有实际操作网络请求只是简单判断返回结果
if (username.length() < 5) {
callback.onError("用户名太短");
} else {
callback.onSuccess("110110");
}
}
}
LoginPresenter
public class LoginPresenter {
private ILoginView mLoginView;
private HttpClient mClient;
public LoginPresenter(@NonNull ILoginView mLoginView,@NonNull HttpClient client) {
this.mLoginView = mLoginView;
this.mClient = client;
}
public void login(String username, String password) {
mClient.login(username, password, new HttpClient.Callback() {
@Override
public void onSuccess(String phone) {
mLoginView.startHomeView();
}
@Override
public void onError(String error) {
mLoginView.onError();
}
});
}
public String getDevice() {
return "nexus6p";
}
}
单元测试,mock出来的对象一定要set到这个LoginPresenter对象中,要不然报错org.mockito.exceptions.misusing.NotAMockException
public class LoginPresenterTest {
private LoginPresenter mPresenter;
private ILoginView mLoginView;
private HttpClient mClient;
//初始化,每测试一个方法都会掉这个
@Before
public void setUp() throws Exception {
mLoginView = Mockito.mock(ILoginView.class);
mClient = Mockito.mock(HttpClient.class);
mPresenter = new LoginPresenter(mLoginView, mClient);
}
@Test
public void login() throws Exception {
//无返回值方法
mPresenter.login("123456", "123456");
Mockito.verify(mClient).login("123456", "123456", null);
}
@Test
public void loginTest() throws Exception {
//无返回值方法
mPresenter.loginTest("123456", "123456");
Mockito.verify(mClient).login("123456", "123456", null);
}
@Test
public void getDevice() throws Exception {
//有返回值方法"
Assert.assertEquals(mPresenter.getDevice(), "nexus6p");
}
}
运行看结果
可以看出login方法有问题,这个错误是正常的,就是我要的结果,可是loginTest方法运行正常,最主要的是他们callback为null,login方法执行的callback不是null,所有login方法执行的时候验证出来就是不一致的。
Comments