在开发过程中,有时候需要对正在开发的某些功能进行测试。我们可以使用很多种方式,这里概括介绍对比几种方式。

junit 4 结合 springboot 常用注解 springboot junit controller_静态方法


第一种就是在当前类中新建一个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目录同级,结构层次如下

junit 4 结合 springboot 常用注解 springboot junit controller_静态方法_02

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单元测试类中就不需要再去重写这个方法,如果重写反而获取不到参数,返回是空。