在开发过程中,有时候需要对正在开发的某些功能进行测试。我们可以使用很多种方式,这里概括介绍对比几种方式。
第一种就是在当前类中新建一个main方法,在main方面里面调用集成的功能,这种方式简单,但是局限性比较多,主要有以下缺点:
/**
* 静态方法
* @param str1
* @return
*/
public static String function1(String str1){
log.info("静态方法:"+str1);
return str1;
}
/**
* 非静态方法
* @param str2
*/
public String function2(String str2){
log.info("非静态方法:"+str2);
return str2;
}
public static void main(String args[]){
log.info("调用静态方法:"+function1("1"));
log.info("调用静态方法:"+function2("2"));//报错:Non-static method 'function2(java.lang.String)' cannot be referenced from a static context
}
(1)当前类中只能有一个main方法,如果开发完某一功能再测试另外一个功能,每次都要修改main方法里面的内容,特别是不方便进行来回切换测试。
(2)main方法不能直接调用service层或者dao层的方法,虽然可以通过上下文的工具类来进行调用,无形之中需要额外编写很多代码,并且这种单元测试的方式与原本的业务实现方式已经有所偏离。失去了进行单元测试的意义。因为原有的功能就是直接调用service层或者dao层的方法,在main方法中进行单元测试却换了另外一种方式调用。
(3)main方法申明的方式是:public static void main,由此可以看出main方法是一个static静态方法,所以在main方法里面无法调用非静态方法,因为Java中的静态方法不能调用非静态方法。具体解释就是:静态方法是属于类的,即静态方法是随着类的加载而加载的,在加载类时,程序就会为静态方法分配内存,而非静态方法是属于对象的,对象是在类加载之后创建的,也就是说静态方法先于对象存在。当你创建一个对象时或者说对象初始化之后,程序才会为其在堆中分配内存,也就是说对于非静态方法,在对象创建的时候程序才会为其分配内存,然后通过类的对象去访问非静态方法。这也就解释了静态方法为什么不能调用非静态方法,因为对象未存在时非静态方法也不存在,静态方法自然不能调用一个不存在的方法,造成资源还没准备好就直接去调用的矛盾。
第二种方式,就是封装成接口,然后把项目运行起来,模拟调用一下接口。这种方式虽然实现起来比较简单,但操作起来比较繁琐,主要有以下原因:
import com.mtons.mblog.shiro.AccountRealm;
import com.mtons.mblog.shiro.AccountSubjectFactory;
import com.mtons.mblog.shiro.AuthenticatedFilter;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.mgt.SubjectFactory;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* shiro权限管理的配置
*
* @author 编程侠
*/
@Configuration
@ConditionalOnProperty(name = "shiro.web.enabled", matchIfMissing = true)
public class ShiroConfiguration {
@Bean
public SubjectFactory subjectFactory() {
return new AccountSubjectFactory();
}
@Bean
public Realm accountRealm() {
return new AccountRealm();
}
@Bean
public CacheManager shiroCacheManager(net.sf.ehcache.CacheManager cacheManager) {
EhCacheManager ehCacheManager = new EhCacheManager();
ehCacheManager.setCacheManager(cacheManager);
return ehCacheManager;
}
/**
* Shiro的过滤器链
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
shiroFilter.setLoginUrl("/login");
shiroFilter.setSuccessUrl("/");
shiroFilter.setUnauthorizedUrl("/error/reject.html");
HashMap<String, Filter> filters = new HashMap<>();
filters.put("authc", new AuthenticatedFilter());
shiroFilter.setFilters(filters);
/**
* 配置shiro拦截器链
*
* anon 不需要认证
* authc 需要认证
* user 验证通过或RememberMe登录的都可以
*
* 顺序从上到下,优先级依次降低
*
*/
Map<String, String> hashMap = new LinkedHashMap<>();
hashMap.put("/dist/**", "anon");
hashMap.put("/theme/**", "anon");
hashMap.put("/storage/**", "anon");
hashMap.put("/login", "anon");
hashMap.put("/interfaceTest/**", "anon");//单独开发指定的接口
hashMap.put("/user/**", "authc");
hashMap.put("/settings/**", "authc");
hashMap.put("/post/editing", "authc");
hashMap.put("/post/submit", "authc");
hashMap.put("/post/delete/*", "authc");
hashMap.put("/post/upload", "authc");
hashMap.put("/admin/channel/list", "authc,perms[channel:list]");
hashMap.put("/admin/channel/update", "authc,perms[channel:update]");
hashMap.put("/admin/channel/delete", "authc,perms[channel:delete]");
hashMap.put("/admin/post/list", "authc,perms[post:list]");
hashMap.put("/admin/post/update", "authc,perms[post:update]");
hashMap.put("/admin/post/delete", "authc,perms[post:delete]");
hashMap.put("/admin/comment/list", "authc,perms[comment:list]");
hashMap.put("/admin/comment/delete", "authc,perms[comment:delete]");
hashMap.put("/admin/user/list", "authc,perms[user:list]");
hashMap.put("/admin/user/update_role", "authc,perms[user:role]");
hashMap.put("/admin/user/pwd", "authc,perms[user:pwd]");
hashMap.put("/admin/user/open", "authc,perms[user:open]");
hashMap.put("/admin/user/close", "authc,perms[user:close]");
hashMap.put("/admin/options/index", "authc,perms[options:index]");
hashMap.put("/admin/options/update", "authc,perms[options:update]");
hashMap.put("/admin/role/list", "authc,perms[role:list]");
hashMap.put("/admin/role/update", "authc,perms[role:update]");
hashMap.put("/admin/role/delete", "authc,perms[role:delete]");
hashMap.put("/admin/theme/*", "authc,perms[theme:index]");
hashMap.put("/admin", "authc,perms[admin]");
hashMap.put("/admin/*", "authc,perms[admin]");
shiroFilter.setFilterChainDefinitionMap(hashMap);
return shiroFilter;
}
}
(1)每次修改代码,为了测试都需要把项目运行起来,等待项目启动也是一个比较耗时的过程,有些项目有各种不同环境的配置,要使项目在本地能顺利运行,得保证有一套本地可以访问的配置。
(2)项目启动运行之后,接口一般都有拦截,可以登录成功之后再调用接口,也可以把当前的接口设置取消拦截,在ShiroFilterFactoryBean里面设置anon不需要认证。这两种方式都不太可取,第一种方式每次都需要登录之后才能模拟调用,第二种方式修改了底层代码,如果误提交上线,该接口就存在安全隐患,无任何拦截。
第三种方式,就是集成junit单元测试,小编比较推荐这种,当然这种方式也有利有弊,先说一下弊端:
(1)junit单元测试的方法只能申明为void,并且不能携带请求参数,这也意味着参数需要写到方法里面,或者提前申明参数,反复测试时可能就需要手动修改参数。
(2)如果你在业务代码中记录日志使用到了如logback技术,在junit单元测试方法里面不能直接使用,有方法可以实现但比较繁琐。
(3)junit单元测试方法里面无法调用非静态方法,只能调用静态方法。
虽然有这些缺陷,但他的优点更突出,主要就是:不需要修改原来任何的业务代码、不需要启动项目就可以直接运行测试方法,多个方法都可以单独测试、可以自由切换环境进行测试等。接下来就重点讲解一下junit单元测试。
1、集成maven依赖,在pom.xml中加入以下代码:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
2、Springboot项目集成junit单元测试,新建test目录文件夹,与main目录同级,结构层次如下
3、编写测试代码,模板如下:
import com.alibaba.fastjson.JSON;
import com.alipay.api.response.AlipayTradePrecreateResponse;
import com.alipay.demo.trade.config.Configs;
import com.alipay.demo.trade.model.ExtendParams;
import com.alipay.demo.trade.model.GoodsDetail;
import com.alipay.demo.trade.model.builder.AlipayTradePrecreateRequestBuilder;
import com.alipay.demo.trade.model.result.AlipayF2FPrecreateResult;
import com.alipay.demo.trade.utils.ZxingUtils;
import com.cn.cy.SpringbootApplication;
import com.cn.cy.common.enums.OrderStatus;
import com.cn.cy.config.AlipayScanConfig;
import com.cn.cy.redis.service.IRedisBaseService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import static com.cn.cy.controller.Alipay.AlipayScanController.tradeService;
/**
* 在类上面加上@SpringBootTest注解。
* 使用@SpringBootTest的时候,需要用classes指定启动类的名字
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = SpringbootApplication.class)
@ActiveProfiles("dev")
public class AlipayScanTest {
private final Logger log = LoggerFactory.getLogger(AlipayScanTest.class);
@Autowired
private AlipayScanConfig alipayScanConfig;
@Autowired
IRedisBaseService redisBaseService;
/**
* junit测试方法——不带请求参数、无返回值
*/
@Test
public void precreateOrder() {
try {
String outTradeNo = "AliPay" + new Date().getTime();//订单号
String subject = "测试订单名称";//订单名称
String totalAmount = "0.03";//订单金额,如果单位是分,则需要转换为元,我这里使用的是元
String body = "订单描述";//订单描述,可以对交易或商品进行一个详细地描述
String operatorId = "test_operator_id";//操作员编号,很重要的参数,可以用作之后的营销
String storeId = "test_store_id";//门店编号,很重要的参数,可以用作之后的营销
ExtendParams extendParams = new ExtendParams();
extendParams.setSysServiceProviderId(Configs.getPid());//商户号
// 商品明细列表
List<GoodsDetail> goodsDetailList = new ArrayList<GoodsDetail>();
GoodsDetail goodsDetail = new GoodsDetail();
goodsDetail.setGoodsId("goodsId");
goodsDetail.setGoodsName("测试商品");
goodsDetail.setPrice(1);
goodsDetail.setQuantity(1);
goodsDetailList.add(goodsDetail);
AlipayTradePrecreateRequestBuilder builder = (new AlipayTradePrecreateRequestBuilder()).setSubject(subject)
.setTotalAmount(totalAmount).setOutTradeNo(outTradeNo).setBody(body)
.setOperatorId(operatorId).setStoreId(storeId)
.setExtendParams(extendParams).setTimeoutExpress(alipayScanConfig.getTimeoutExpress())
.setNotifyUrl(alipayScanConfig.getNotifyUrl()).setGoodsDetailList(goodsDetailList);
//发起向支付宝服务端预创建请求,并返回创建结果
AlipayF2FPrecreateResult alipayF2FPrecreateResult = tradeService.tradePrecreate(builder);
log.info("预创建订单响应内容:" + JSON.toJSONString(alipayF2FPrecreateResult));
switch (alipayF2FPrecreateResult.getTradeStatus()) {
case SUCCESS:
redisBaseService.setValue(outTradeNo, OrderStatus.pre_create.getCode(), 1);
log.info(outTradeNo + "支付宝订单预创建成功");
AlipayTradePrecreateResponse alipayTradePrecreateResponse = alipayF2FPrecreateResult.getResponse();
String filePath = String.format(alipayScanConfig.getDownloadPath() +"/qr-%s.png", new Object[] { alipayTradePrecreateResponse.getOutTradeNo() });
//将二维码保存到本地filePath目录
ZxingUtils.getQRCodeImge(alipayTradePrecreateResponse.getQrCode(), 256, filePath);
break;
case FAILED:
log.error(outTradeNo + "支付宝预下单失败");
break;
case UNKNOWN:
log.error(outTradeNo + "支付宝预下单,系统异常,预下单状态未知");
break;
default:
log.error(outTradeNo + "支付宝预下单,不支持的交易状态,交易返回异常");
break;
}
} catch (Exception e) {
log.error("系统抛出异常");
}
}
}
详细说明:
(1)junit单元测试类上要加一些注解,主要注解说明如下:
@RunWith就是一个运行器,@RunWith(SpringRunner.class)指让类运行在Spring的测试环境,以便测试开始时自动创建Spring应用上下文。
@SpringBootTest替代了spring-test中的@ContextConfiguration注解,目的是加载ApplicationContext,启动spring容器。
@ActiveProfiles指定配置环境,我设置了四种环境,开发【dev】、测试【test】、演示【uat】和生产【pro】。在@ActiveProfiles注解上指定对应的配置就会使用对应的配置参数来执行方法。
(2)日志记录,最开始已经说明了在junit单元测试方法里面不能直接使用logback,我改用通用类日志LoggerFactory,也可以实现控制台的日志打印。
(3)在junit单元测试方法上,需要添加注解@Test,他的作用就是该方法可以不用main方法调用就可以测试出运行结果,是一种测试方法。
(4)在junit测试方法里面可以直接调用service方法或者dao层方法,跟正常的控制类controller里面调用一样。
(5)junit测试方法里面只能调用静态方法。
(6)junit测试方法不能带参,无返回值。
注意事项:有些项目在启动时需要去加载一些配置文件,比如我在项目启动时需要加载支付宝的参数文件zfbinfo_dev.properties,加载完之后才能获取到参数。在junit单元测试类中就不需要再去重写这个方法,如果重写反而获取不到参数,返回是空。