关于单测的笔记

之前没有写单元测试的意识,把测试框架看成不用启动项目的带Spring框架的类。。

后来项目有了单元测试覆盖率要求,于是开始根据数据库的现有数据写用例

然后被教育了,单元测试应该是和外部环境无关的对现有逻辑的测试,所有数据要在@Before方法构建,并且测试完之后所有操作都要回滚。。

灵异事件二则

……调微信支付的时候碰到两个灵异事件

首先是按Spring文档 添加了如下依赖:

1
2
3
dependencies {
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
}

然后写了一个微信支付AppID的配置类

结果代码里跑起来死活为null

想了一下,把wxpay相关的配置拿到了spring相关配置下方(按理应该没影响)

然后配置文件里log后面莫名其妙多了一个192.168.3.49,没注意到,启动测试类读取微信支付配置时提示连不上数据库192.168.3.49……

删掉之后 配置文件里的数据库密码突然变成了之前调试本地数据库时候的本地数据库密码。。

改掉配置文件,invalid cache and restart之后测试类能读到配置类的数据了。。然后配置类的@Data注解上还多了个断点,感觉电脑被远程控制了一样……

最骚的是,IDEA 2019.1.1不支持Gradle4.9+SpringBoot读配置文件的组合,如果依赖写的是annotationProcessor会提示class path里没有spring配置相关的processor。。


2019-5-21 17:12:36
更骚的出现了,测试类能拿到appID,引用了common模块的其他模块跑起来的时候拿不到

最后是因为多个application.yml冲突了……

微信小程序支付对接开发笔记

2019-9-23更新
试了一下支付宝

发现支付宝网页有个收银台,PC网站下单完事之后会给一个自动提交的form,然后302跳转到另一个URL

感觉实际对接的时候可以把那个URL返回给前端


新单位新项目,微信小程序对接微信支付+退款,由于开发测试是真的繁琐,在此记录一下笔记。

下载微信SDK,编写后端代码

……随便吐槽一下,微信SDK被人挖出来漏洞之后就不放github了,只能从微信官网下载,然后阿里规约扫描报了一大堆错……也没法改,万一后续SDK更新了呢

2019-8-28更新:
我真是认识鹅厂微信支付Java SDK作者的美,你弄个WxPayConfig的抽象类给我们扩展,然后getAppId()这种方法的访问权限是default,用Maven等依赖管理引入之后,根本没法扩展,因为必须得在同一个包下。。

然后其他WxPay之类的类又必须依赖这个类的子类才能干活。。 难怪官方都只给源码下载,而不是托管到什么依赖仓库。。

配置类

首先创建一个SDK内WxPayConfig类的实现类,由于项目名是微商城,取名叫WscWxConfig

使用Spring配置读取的方式,在类里声明AppIdMchId(商户号),Key,证书内容(字节数组)几个成员变量,然后AppId MchId keygetter直接用lombok生成。

getCertStream这个方法需要重写为return new ByteArrayInputStream(this.certData);的形式,getWxPayDomain更麻烦:

1
2
3
4
5
6
7
8
9
10
11
12
13
IWXPayDomain getWXPayDomain() {
return new IWXPayDomain() {
@Override
public void report(String domain, long elapsedTimeMillis, Exception ex) {

}
@Override
public DomainInfo getDomain(WXPayConfig config) {
return new DomainInfo(WXPayConstants.DOMAIN_API, true);
}
};
}

2019-8-28更新:
后续改成按公司独立设置微信支付参数,所以用了ThreadLocal维护这些东西。。支付退款证书也放在了OSS上,反正是流,无所谓……

顺带给WxPay类做一个单例,声明一个WxPay类型的变量,然后用@PostConstruct注解初始化方法:
注意这里的构造函数传参第三个参数,表示使用沙箱环境

1
2
3
4
5
6
7
public void initWxPay(){
try {
this.wxPay = new WXPay(this, false, true);
} catch (Exception e) {
log.error("inital wxpay failed ", e);
}
}

这样配置类就写完了。

Service

微信SDK给了轻度封装,至少使用Map<String, String>就可以完成参数填写了

统一下单

由于负责用户模块的同事的设计是不存储OpenId,因此该方法直接接受code,订单号,金额,公司名(用于微信订单描述)作为参数。

接下来是调用微信SDK发送请求,并且将拿到的prepay_id返回前端。

接受支付通知

一定要校验签名和金额!!!!

幂等性处理后,将接收到的支付信息持久化一份到数据库,并且根据业务结果+支付金额调订单Service。

扯一下我们的幂等性方案吧,防重入一开始打算用悲观锁锁住订单号对应的行,但是怕应用意外崩溃然后锁无法释放,最后用的是redis的setnx。防重入之后就做一下状态判断就行了。

退款

这回是由商户服务器发起请求,所以不用OpenId了,指定订单号、金额、退款金额、售后单号即可

接受退款通知

类似支付通知的幂等性处理,持久化一份到数据库,并根据业务结果调售后Service。

Controller

主要是两个接收通知的接口,以及退款接口信息的解密方法。

先从request.getInputStream()中获取XML,然后用微信SDK转成Map,再用微信SDK做签名校验。

测试

后端码狗自测微信小程序支付接口简直要人命……居然没找到描述全流程的博文
调个微信支付和玩tmd解谜RPG一样,到处找线索

小程序支付交付需要在沙箱模拟过一遍之后向微信提交验收通过申请,当然如果已经有通过微信支付验收的小程序就不必走一遍沙箱流程了。

写完代码自己测试时,一般还是直接用一个能用的小程序支付参数,而不是沙箱,理由后面讲。。

数据准备

AppId

小程序所属的公司账号将开发者的微信号加入小程序开发者后,开发者使用微信扫码登录微信小程序后台即可看到。

AppSecret

需要超级管理员在小程序后台开发设置生成,用于小程序Code换OpenId用。。

MchId

在商户后台将开发者账号加入商户的员工账号后,员工账号会收到包含MchId的通知。

Key

商户后台API安全处填写,需要超级管理员验证,需要商户账号开通操作密码
其中沙箱开发要用到SignKey,使用Key请求微信的API生成。

解析退款信息时使用。

证书

商户后台API安全处可以重设,最好能找到第一次申请的证书文件。
解析退款信息时使用。

内网穿透

本地请求支付的接口需要一个带https的内网穿透wx.request用,即使有CI/CD,也懒得改一行代码提一个pr……图省事直接买了NATAPP。

吐槽一下,要调试小程序需要买他的国内隧道+二级域名,15元一年的域名+9元一个月的隧道;

倒是可以选择用自己的域名,但是我域名没备案……而阿里云买的域名要备案需要配阿里云的实例……

还有香港流量包月的隧道,买完才发现这个不能开https……香港流量不包月的倒是可以配自己域名,但是自己域名443端口必须空着,我域名有个开了https的小网盘跑着……

幸好微信的支付通知回调可以不用https,就用免费隧道顶上。

2019-8-28更新:
后续在自己的服务器上搭了个ngrok,不用额外付冤枉钱了。。

统一下单

在微信开发者工具中新建一个小程序,修改AppId为公司小程序的AppId(也可以直接调公司小程序的代码,但是单独测一个支付我还是选择新起一个小程序)。

加一个按钮,测试支付,绑定一个函数,把返回的requestPayment用的数据打印出来,顺带请求支付:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
testPay: function (e) {
wx.login({
success(res) {
if (res.code) {
// 发起网络请求
wx.request({
url: 'https://qxkjwxpay.mynatapp.cc/micro/order/payOrder',
header: {
'Authorization': '登录接口生成的JWT内容'
},
method: 'POST',
data: {
code: res.code,
amount: '1.01',
orderNum: '2',
paymentType: 10
},
success(res) {
console.log(res);
wx.requestPayment(
{
'timeStamp': res.data.data.timestamp,
'nonceStr': res.data.data.nonceStr,
'package': res.data.data.prepayId,
'signType': 'MD5',
'paySign': res.data.data.sign,
'success': function (res) { },
'fail': function (res) { },
'complete': function (res) { }
})
}
})
} else {
console.log('登录失败!' + res.errMsg)
}
}
})
}

结果是这样的:

1
2
3
4
5
6
7
8
9
code:200
data:{
nonceStr:"klrv5kgx5dJdc7ILG9Uj9k70hUKzXbF8"
prepayId:"prepay_id=wx20190522095426670409"
sign:"A118797887A33C961594D149285EDCEE"
timestamp:1558490065
}
msg:"成功"
subCode:null

调起支付

果↓然↑啊,扫描开发者工具给的二维码时提示错误:调用支付JSAPI缺少参数:total_fee

我寻思统一下单都成功了你在说你吗呢,会报错你就多报几句,是不是把你妈杀了你也只能呜咽着说出你缺少total_fee?真的憨批一样

package传的也带了prepay_id=,后端生成随机数+签名用的是微信JavaSDK的工具类,签名字段大小写也没问题

发现后端生成签名时,工具类会自动拼上key=key,而我手动在map里加了一个key……也就是现在的签名内容有问题。。

改掉之后还是报错,看到微信开放社区里一个帖子 说即使报错也受到了微信支付的成功通知,又看到segmentfault里的一个帖子沙箱就是这样。。

我真是艹了

接受回调

坑点:微信SDK送了一个判断支付结果通知中的sign是否有效的方法isPayResultNotifySignatureValid(),这方法默认没有传入签名类型的时候,选的是MD5.

但是微信支付主工具类 初始化签名方式时,根据传入是否沙箱来切换加密方式和URL,此时加密方式是HMAC!!

1
2
3
4
5
6
7
8
9
10
11
12
13
public WXPay(final WXPayConfig config, final String notifyUrl, final boolean autoReport, final boolean useSandbox) throws Exception {
this.config = config;
this.notifyUrl = notifyUrl;
this.autoReport = autoReport;
this.useSandbox = useSandbox;
if (useSandbox) {
this.signType = SignType.MD5; // 沙箱环境
}
else {
this.signType = SignType.HMACSHA256;
}
this.wxPayRequest = new WXPayRequest(config);
}

于是支付回调返回的sign也当然是HMAC,但是支付回调是没有写明签名类型的……这里一定做处理,比较优雅的做法是接受回调时读取配置加入签名类型。。不过我当时是直接改了微信的SDK……

然后是支付回调里没有trade_state,只有result_code,因为只推送成功结果,这点要和主动拉取支付结果做区分。

退款回调

发起退款的流程和发起支付类似,略过不表

退款回调没有sign字段,而且需要JDK装密钥长度无限扩展包

直接解密req_info字段即可,解密过程有几个坑,MD5哈希之后要转成小写,算法名是AES/ECB/PKCS7Padding不能写错,如果抛出需要IV的异常就是算法名写错了……

解密代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    private String decrypt(String reqInfo) throws Exception {
// (1)对加密串A做base64解码,得到加密串B
byte[] encryptB = Base64.getDecoder().decode(reqInfo);
// (2)对商户key做md5,得到32位小写key* ( key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置 )
StringBuilder hexString = new StringBuilder();
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(wscWxConfig.getKey().getBytes());
byte[] hash = md.digest();
for (byte b : hash) {
if ((0xff & b) < 0x10) {
hexString.append("0").append(Integer.toHexString((0xFF & b)));
} else {
hexString.append(Integer.toHexString(0xFF & b));
}
}
String md5Key = hexString.toString().toLowerCase();
// (3)用key*对加密串B做AES-256-ECB解密(PKCS7Padding)
// 需要JDK中添加JCE:https://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html
Security.addProvider(new BouncyCastleProvider());
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS7Padding");
Key sKeySpec = new SecretKeySpec(md5Key.getBytes(StandardCharsets.UTF_8), "AES");
cipher.init(Cipher.DECRYPT_MODE, sKeySpec);
byte[] result = cipher.doFinal(encryptB);
return new String(result);
}

Spring Boot项目公用模块单元测试踩坑

现在手头的项目分为几个子模块,前台一个,后台一个,计划任务一个,Service、bo、dao等放在common模块里,而common模块作为其他几个模块的依赖存在。

在加入Spring环境的单元测试时报了错:

1
java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=...) with your test

按提示在测试类加入@SpringBootConfiguration后,报错变了:

1
Parameter 0 of method setXXService in com.XXX.common.XXXServiceImplTest required a bean of type 'com.XXX.common.XXXService' that could not be found.

Google了一下,发现这种情况是Application启动类不存在导致的,可是既然是依赖,自然就没有Application启动类,于是我尝试加入@SpringBootTest(classes="要测试的Service"),然后发现该Service注入到测试类成功了,但是Service中的几个成员注入失败,报错是找不到Bean。

查了一下发现,单元测试执行时,Spring会扫描这个class指定的类所在的包的所有子包,也就是一定要有一个打了@SpringBootApplication注解的类存在于common包下!

先建一个Root.class应付了事,然后慢慢研究:

在测试类加入注解@ComponentScan(basePackages = {"com.XXX.common"}),这回报错又变了:

1
Parameter 0 of method setXXXMapper in com.XXX.common.service.impl.XXXServiceImpl required a bean of type 'com.XXX.common.domain.repository.XXXMapper' that could not be found.

项目中用了Mybatis-Plus,该Mapper接口继承了BaseMapper<>,加了@Mapper和@Repository注解,难道@Repository注解不认??

暂且先改为@Component,报错变成了这样:

1
2
Parameter 0 of method setObjectMapper in com.XXX.common.service.impl.XXXServiceImpl required a bean of type 'com.fasterxml.jackson.databind.ObjectMapper' that could not be found.

原来是忘记把Jackson的ObjectMapper纳入Spring 管理了:

1
2
3
4
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}

接下来依然是Mapper接口报错找不到Bean……无奈先用Root.class顶过去,日后再说)

2019-8-28更新

最后的解决方案是不在Common模块里写测试用例……