.Net单元测试业务实践
使用次数和允许取消次数单元测试实践
/**
* prism.js Github theme based on GitHub's theme.
* @author Sam Clarke
*/
code[class*="language-"],
pre[class*="language-"] {
color: #333;
background: none;
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.4;
-moz-tab-size: 8;
-o-tab-size: 8;
tab-size: 8;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*="language-"] {
padding: .8em;
overflow: auto;
/* border: 1px solid #ddd; */
border-radius: 3px;
/* background: #fff; */
background: #f5f5f5;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
background: #f5f5f5;
}
.token.comment,
.token.blockquote {
color: #969896;
}
.token.cdata {
color: #183691;
}
.token.doctype,
.token.punctuation,
.token.variable,
.token.macro.property {
color: #333;
}
.token.operator,
.token.important,
.token.keyword,
.token.rule,
.token.builtin {
color: #a71d5d;
}
.token.string,
.token.url,
.token.regex,
.token.attr-value {
color: #183691;
}
.token.property,
.token.number,
.token.boolean,
.token.entity,
.token.atrule,
.token.constant,
.token.symbol,
.token.command,
.token.code {
color: #0086b3;
}
.token.tag,
.token.selector,
.token.prolog {
color: #63a35c;
}
.token.function,
.token.namespace,
.token.pseudo-element,
.token.class,
.token.class-name,
.token.pseudo-class,
.token.id,
.token.url-reference .token.variable,
.token.attr-name {
color: #795da3;
}
.token.entity {
cursor: help;
}
.token.title,
.token.title .token.punctuation {
font-weight: bold;
color: #1d3e81;
}
.token.list {
color: #ed6a43;
}
.token.inserted {
background-color: #eaffea;
color: #55a532;
}
.token.deleted {
background-color: #ffecec;
color: #bd2c00;
}
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
/* JSON */
.language-json .token.property {
color: #183691;
}
.language-markup .token.tag .token.punctuation {
color: #333;
}
/* CSS */
code.language-css,
.language-css .token.function {
color: #0086b3;
}
/* YAML */
.language-yaml .token.atrule {
color: #63a35c;
}
code.language-yaml {
color: #183691;
}
/* Ruby */
.language-ruby .token.function {
color: #333;
}
/* Markdown */
.language-markdown .token.url {
color: #795da3;
}
/* Makefile */
.language-makefile .token.symbol {
color: #795da3;
}
.language-makefile .token.variable {
color: #183691;
}
.language-makefile .token.builtin {
color: #0086b3;
}
/* Bash */
.language-bash .token.keyword {
color: #0086b3;
}html body{font-family:"Helvetica Neue",Helvetica,"Segoe UI",Arial,freesans,sans-serif;font-size:16px;line-height:1.6;color:#333;background-color:#fff;overflow:initial;box-sizing:border-box;word-wrap:break-word}html body>:first-child{margin-top:0}html body h1,html body h2,html body h3,html body h4,html body h5,html body h6{line-height:1.2;margin-top:1em;margin-bottom:16px;color:#000}html body h1{font-size:2.25em;font-weight:300;padding-bottom:.3em}html body h2{font-size:1.75em;font-weight:400;padding-bottom:.3em}html body h3{font-size:1.5em;font-weight:500}html body h4{font-size:1.25em;font-weight:600}html body h5{font-size:1.1em;font-weight:600}html body h6{font-size:1em;font-weight:600}html body h1,html body h2,html body h3,html body h4,html body h5{font-weight:600}html body h5{font-size:1em}html body h6{color:#5c5c5c}html body strong{color:#000}html body del{color:#5c5c5c}html body a:not([href]){color:inherit;text-decoration:none}html body a{color:#08c;text-decoration:none}html body a:hover{color:#00a3f5;text-decoration:none}html body img{max-width:100%}html body>p{margin-top:0;margin-bottom:16px;word-wrap:break-word}html body>ul,html body>ol{margin-bottom:16px}html body ul,html body ol{padding-left:2em}html body ul.no-list,html body ol.no-list{padding:0;list-style-type:none}html body ul ul,html body ul ol,html body ol ol,html body ol ul{margin-top:0;margin-bottom:0}html body li{margin-bottom:0}html body li.task-list-item{list-style:none}html body li>p{margin-top:0;margin-bottom:0}html body .task-list-item-checkbox{margin:0 .2em .25em -1.8em;vertical-align:middle}html body .task-list-item-checkbox:hover{cursor:pointer}html body blockquote{margin:16px 0;font-size:inherit;padding:0 15px;color:#5c5c5c;border-left:4px solid #d6d6d6}html body blockquote>:first-child{margin-top:0}html body blockquote>:last-child{margin-bottom:0}html body hr{height:4px;margin:32px 0;background-color:#d6d6d6;border:0 none}html body table{margin:10px 0 15px 0;border-collapse:collapse;border-spacing:0;display:block;width:100%;overflow:auto;word-break:normal;word-break:keep-all}html body table th{font-weight:bold;color:#000}html body table td,html body table th{border:1px solid #d6d6d6;padding:6px 13px}html body dl{padding:0}html body dl dt{padding:0;margin-top:16px;font-size:1em;font-style:italic;font-weight:bold}html body dl dd{padding:0 16px;margin-bottom:16px}html body code{font-family:Menlo,Monaco,Consolas,'Courier New',monospace;font-size:.85em !important;color:#000;background-color:#f0f0f0;border-radius:3px;padding:.2em 0}html body code::before,html body code::after{letter-spacing:-0.2em;content:"\00a0"}html body pre>code{padding:0;margin:0;font-size:.85em !important;word-break:normal;white-space:pre;background:transparent;border:0}html body .highlight{margin-bottom:16px}html body .highlight pre,html body pre{padding:1em;overflow:auto;font-size:.85em !important;line-height:1.45;border:#d6d6d6;border-radius:3px}html body .highlight pre{margin-bottom:0;word-break:normal}html body pre code,html body pre tt{display:inline;max-width:initial;padding:0;margin:0;overflow:initial;line-height:inherit;word-wrap:normal;background-color:transparent;border:0}html body pre code:before,html body pre tt:before,html body pre code:after,html body pre tt:after{content:normal}html body p,html body blockquote,html body ul,html body ol,html body dl,html body pre{margin-top:0;margin-bottom:16px}html body kbd{color:#000;border:1px solid #d6d6d6;border-bottom:2px solid #c7c7c7;padding:2px 4px;background-color:#f0f0f0;border-radius:3px}@media print{html body{background-color:#fff}html body h1,html body h2,html body h3,html body h4,html body h5,html body h6{color:#000;page-break-after:avoid}html body blockquote{color:#5c5c5c}html body pre{page-break-inside:avoid}html body table{display:table}html body img{display:block;max-width:100%;max-height:100%}html body pre,html body code{word-wrap:break-word;white-space:pre}}.markdown-preview{width:100%;height:100%;box-sizing:border-box}.markdown-preview .pagebreak,.markdown-preview .newpage{page-break-before:always}.markdown-preview pre.line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}.markdown-preview pre.line-numbers>code{position:relative}.markdown-preview pre.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:1em;font-size:100%;left:0;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.markdown-preview pre.line-numbers .line-numbers-rows>span{pointer-events:none;display:block;counter-increment:linenumber}.markdown-preview pre.line-numbers .line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}.markdown-preview .mathjax-exps .MathJax_Display{text-align:center !important}.markdown-preview:not([for="preview"]) .code-chunk .btn-group{display:none}.markdown-preview:not([for="preview"]) .code-chunk .status{display:none}.markdown-preview:not([for="preview"]) .code-chunk .output-div{margin-bottom:16px}.scrollbar-style::-webkit-scrollbar{width:8px}.scrollbar-style::-webkit-scrollbar-track{border-radius:10px;background-color:transparent}.scrollbar-style::-webkit-scrollbar-thumb{border-radius:5px;background-color:rgba(150,150,150,0.66);border:4px solid rgba(150,150,150,0.66);background-clip:content-box}html body[for="html-export"]:not([data-presentation-mode]){position:relative;width:100%;height:100%;top:0;left:0;margin:0;padding:0;overflow:auto}html body[for="html-export"]:not([data-presentation-mode]) .markdown-preview{position:relative;top:0}@media screen and (min-width:914px){html body[for="html-export"]:not([data-presentation-mode]) .markdown-preview{padding:2em calc(50% - 457px)}}@media screen and (max-width:914px){html body[for="html-export"]:not([data-presentation-mode]) .markdown-preview{padding:2em}}@media screen and (max-width:450px){html body[for="html-export"]:not([data-presentation-mode]) .markdown-preview{font-size:14px !important;padding:1em}}@media print{html body[for="html-export"]:not([data-presentation-mode]) #sidebar-toc-btn{display:none}}html body[for="html-export"]:not([data-presentation-mode]) #sidebar-toc-btn{position:fixed;bottom:8px;left:8px;font-size:28px;cursor:pointer;color:inherit;z-index:99;width:32px;text-align:center;opacity:.4}html body[for="html-export"]:not([data-presentation-mode])[html-show-sidebar-toc] #sidebar-toc-btn{opacity:1}html body[for="html-export"]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc{position:fixed;top:0;left:0;width:300px;height:100%;padding:32px 0 48px 0;font-size:14px;box-shadow:0 0 4px rgba(150,150,150,0.33);box-sizing:border-box;overflow:auto;background-color:inherit}html body[for="html-export"]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc::-webkit-scrollbar{width:8px}html body[for="html-export"]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc::-webkit-scrollbar-track{border-radius:10px;background-color:transparent}html body[for="html-export"]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc::-webkit-scrollbar-thumb{border-radius:5px;background-color:rgba(150,150,150,0.66);border:4px solid rgba(150,150,150,0.66);background-clip:content-box}html body[for="html-export"]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc a{text-decoration:none}html body[for="html-export"]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc ul{padding:0 1.6em;margin-top:.8em}html body[for="html-export"]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc li{margin-bottom:.8em}html body[for="html-export"]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc ul{list-style-type:none}html body[for="html-export"]:not([data-presentation-mode])[html-show-sidebar-toc] .markdown-preview{left:300px;width:calc(100% - 300px);padding:2em calc(50% - 457px - 150px);margin:0;box-sizing:border-box}@media screen and (max-width:1274px){html body[for="html-export"]:not([data-presentation-mode])[html-show-sidebar-toc] .markdown-preview{padding:2em}}@media screen and (max-width:450px){html body[for="html-export"]:not([data-presentation-mode])[html-show-sidebar-toc] .markdown-preview{width:100%}}html body[for="html-export"]:not([data-presentation-mode]):not([html-show-sidebar-toc]) .markdown-preview{left:50%;transform:translateX(-50%)}html body[for="html-export"]:not([data-presentation-mode]):not([html-show-sidebar-toc]) .md-sidebar-toc{display:none}
/* Please visit the URL below for more information: */
/* https://shd101wyy.github.io/markdown-preview-enhanced/#/customize-css */
业务简述
关键字段:邀请码最大使用次数UseMaxNumber和允许取消次数CancelUseMaxNumber,已使用次数UsedCount,已取消次数CancelUsedCount。
提交使用邀请码的订单,占用邀请码使用次数。
在允许取消次数内取消订单,退回邀请码使用次数。
超过允许取消次数取消订单,不退回邀请码使用次数。注意点:临界值。
原核心代码(X.1版)
public ResponseMessage<bool> 示例方法_ProcessCode(X used,YY invitecodedto)
{
var isoverinvite = false;//已经超过取消次数
var iswilloverinvite = false;//将要超出取消次数
long inviteNum = 0;//本次邀约使用次数
//判断是否已经超过取消次数,或者将要超出取消次数。
if (invitecodedto != null && invitecodedto.IsLimitCancelUse)
{
if (invitecodedto.CancelUsedCount > invitecodedto.CancelUseMaxNumber)
{
isoverinvite = true;
}
else if (invitecodedto.CancelUsedCount + used.InviteNum > invitecodedto.CancelUseMaxNumber)
{
iswilloverinvite = true;
}
} ResponseMessage<long> inviteuseres = null;
//邀约码不为null,递增取消次数,扣减使用次数。
if (invitecodedto != null)
{
//递增已取消次数
var cancelcount = _codeService.IncCancelUseCount(invitecodedto.Id, (int)used.InviteNum);
if (isoverinvite)
{ }
else if (iswilloverinvite)
{
inviteNum = invitecodedto.CancelUseMaxNumber > cancelcount.Body ? invitecodedto.CancelUseMaxNumber - cancelcount.Body : cancelcount.Body - invitecodedto.CancelUseMaxNumber;
//将要超出的,只退出部分。
inviteuseres = _codeService.IncUseCount(invitecodedto.Id, -(int)(inviteNum));
}
else
{
inviteNum = used.InviteNum;
//未超出取消次数的,全数退回。
inviteuseres = _codeService.IncUseCount(invitecodedto.Id, -(int)inviteNum);
}
}
.
.
.
//更新取消日志。
//更新码相关的各种状态。
}
X.1版代码引起问题
使用次数为1,允许取消次数为1时,运行正确。
使用次数为1,允许取消次数为2时,结果错误。
>>测试流程目标:【每次报名都为1人】报名一次,取消一次,再报名一次,再取消一次后。再报名一次后,后续不能再报名。
>>实际效果:仍然还能报名一次。
>>原因分析:订单第二次取消后。已取消次数为2,允许取消次数为2,这个判断无法命中。
if (invitecodedto.CancelUsedCount > invitecodedto.CancelUseMaxNumber)
{
isoverinvite = true;
}
优化后代码(X.2版)
var isoverinvite = false;//已经超过取消次数
var iswilloverinvite = false;//将要超出取消次数
long inviteNum = 0;//本次邀约使用次数
if (invitecodedto != null && invitecodedto.IsLimitCancelUse)
{
//这里多加了个=号
if (invitecodedto.CancelUsedCount >= invitecodedto.CancelUseMaxNumber)
{
isoverinvite = true;
}//这里也多加了个=号
else if (invitecodedto.CancelUsedCount + used.InviteNum >= invitecodedto.CancelUseMaxNumber)
{
iswilloverinvite = true;
}
}
X.2版代码引起问题
X.2版修复了上个问题。但仍有场景覆盖不够。
使用次数为2,允许取消次数为2时,结果错误。
>>测试流程目标:报名一次(1人),取消,再报名一次(2人),再取消。预期仍可以继续报名1人。
>>实际效果:无法继续报名。
>>原因分析,第二次取消请求时:
>>>根据判断 已取消次数加上邀约人数大于允许取消次数,1+2>2,所以是将要超出允许取消次数。
.
.
else if (invitecodedto.CancelUsedCount + used.InviteNum > invitecodedto.CancelUseMaxNumber)
{
iswilloverinvite = true;
}
.
.
>>>再来看下扣减使用次数的部分。CancelUseMaxNumber为2,cancelcount.Body为2,
>>>所以结果是:2>2?(2-2):(2-2),返回0,意思是没有返回使用次数。
.
.
else if (iswilloverinvite)
{
inviteNum = invitecodedto.CancelUseMaxNumber > cancelcount.Body ? invitecodedto.CancelUseMaxNumber - cancelcount.Body : cancelcount.Body - invitecodedto.CancelUseMaxNumber;
//将要超出的,只退出部分。
inviteuseres = _codeService.IncUseCount(invitecodedto.Id, -(int)(inviteNum));
}
.
.
>>>正确结果应该是:因为已经取消过一次了,这次报名2人,如按正常应该是总取消3次,但允许取消次数是2次,所以使用次数只能返回一次。
>>>预期结果和实际结果不符。
思考
上面问题是由于退回使用次数计算不对引起的。
改动后验证流程是很繁琐的,要配置邀请码,要填写报名信息,要重复提交,重复取消订单好几次来验证逻辑。
组合条件是千变万化的。
这个业务重点是测试取消订单后对于使用次数和允许取消次数的正确性。如全流程走一下,是浪费时间的。
所以为保证正确性及方便,这个必须支持单元测试。单元测试才能快速试错。
影响单元测试的几点
业务耦合。这个取消邀请方法内有处理邀请码使用次数和取消次数的,也有处理取消记录,维护各个状态等。不符合单一功能原则。
数据库依赖,影响mock数据及执行后的结果对比。
重复执行后结果的积累。如订单取消后,邀请码的使用次数和允许取消次数都会变,作为下次单元测试的依据。
改进建议
对打算单元测试的代码,要保持功能单一,不耦合其他业务。
面向接口编程,依赖注入。与具体的实现解耦,方便单元测试。
方法体尽量移除仓储部分逻辑或者mock一个仓储对象替代。
必须方便批量单元测试。
单元测试前置--Nuget包依赖
Xunit:一个开发测试框架,它支持测试驱动开发,具有极其简单和与框架特征对齐的设计目标。
xunit.runner.visualstudio: 支持Vs调试,运行测试
NSubstitute :一个友好的.net单元测试隔离框架。
Autofac: Ioc容器
//单元测试部分
public class GetTicketDiscounts_Test
{
private IXTaDiscountService discountService = null;
private IXTaCodeService codeSub = null;
public GetTicketDiscounts_Test()
{
discountService = XTaContainer.Resolve<IXTaDiscountService>();
codeSub = NSubstitute.Substitute.For<IXTaCodeService>();
}
}
//注册部分
public static class XTaContainer
{
public readonly static IContainer _container;
static XTaContainer()
{
// Create your builder.
var builder = new ContainerBuilder();
//自动注册。
var baseType = typeof(IApplication);
var assemblys = AppDomain.CurrentDomain.GetAssemblies().ToList(); builder.RegisterAssemblyTypes(assemblys.ToArray())
.Where(t => baseType.IsAssignableFrom(t) && t != baseType)
.AsImplementedInterfaces()
.InstancePerLifetimeScope();
//Redis
builder.Register(n => Substitute.For<ICache>())
.As<ICache>().SingleInstance();
//mongodb
builder.Register(n => Substitute.For<IMongoDbProvider>())
.As<IMongoDbProvider>().SingleInstance();
_container = builder.Build();
}
public static T Resolve<T>()
{
return _container.Resolve<T>();
}
}
支持单元测试的代码(X.3版-只粘贴相关代码)
//接口
public interface IXTaService : IApplication{
ResponseMessage<long> GetReturnUseNum(long invitediscountNum, XTaCodeDto codedto);
}
//实现
public class XTaDiscountService : IXTaDiscountService
{
private readonly IXTaCodeService _codeService;
public XTaDiscountService(
IXTaCodeService codeService)
{
_codeService = codeService;
}
//将操作使用次数和取消次数的仓储部分挪出去,这里只计算需要退回的使用次数。
public ResponseMessage<long> GetReturnUseNum(long invitediscountNum, XTaCodeDto codedto)
{
//默认是全部退回使用次数。
long returnNum = invitediscountNum;
if (codedto == null)
{
return ResponseMessage<long>.MakeSucc(0);
}
//不限制取消的的时候,退回全部使用次数。
if (!codedto.IsLimitCancelUse)
{
return ResponseMessage<long>.MakeSucc(returnNum);
}
//已超过的不处理。
if (codedto.CancelUsedCount >= codedto.CancelUseMaxNumber)
{
return ResponseMessage<long>.MakeSucc(0);
}
//将要超过的。
if (codedto.CancelUsedCount + invitediscountNum >= codedto.CancelUseMaxNumber)
{
returnNum = codedto.CancelUsedCount + invitediscountNum - codedto.CancelUseMaxNumber;
return ResponseMessage<long>.MakeSucc(returnNum);
}
return ResponseMessage<long>.MakeSucc(returnNum);
}
}
>初始化数据 private void 验证取消优惠_初始化数据(ref XTaCodeDto codeDto, int usemax = 0, int cancelmax = 0)
{
if (codeDto == null)
{
codeDto = new XTaCodeDto()
{
Id = "11111",
CancelUsedCount = 0,
UsedCount = 0,
PrivateSetting = new PrivateSetting()
{
IsLimitCancelUse = true,
IsCustomCancelUse = true,
CancelUseMaxNumber = 1, IsLimitUse = true,
IsCustomUse = true,
UseMaxNumber = 1
}
};
}
if (cancelmax > 0)
{
codeDto.PrivateSetting.CancelUseMaxNumber = cancelmax;
codeDto.CancelUsedCount = 0;
}
if (usemax > 0)
{
codeDto.PrivateSetting.UseMaxNumber = usemax;
codeDto.UsedCount = 0;
}
}
> 模拟报名使用邀请码,递增使用次数,方便批量测试。 private void 初始化数据_模拟报名使用邀请码_递增使用次数(int useNum, XTaCodeDto codeDto)
{
//mock模拟使用邀请码时,递增的邀请码使用次数返回使用次数。
var usercount = codeSub.IncUseCount(codeDto.Id, Arg.Any<int>()).Returns(x => new ResponseMessage<long>() { Body = (int)codeDto.UsedCount + x.Arg<int>() });
codeDto.UsedCount = codeSub.IncUseCount(codeDto.Id, useNum).Body;
}
> 模拟取消订单,退回使用次数 private void 验证取消优惠_退回使用次数_V1ForPrivate(long inviteDiscountNum, XTaCodeDto codeDto)
{
//计算退回使用次数。
var res = discountService.GetReturnUseNum(inviteDiscountNum, codeDto);
codeDto.UsedCount -= res.Body;
codeDto.CancelUsedCount += inviteDiscountNum;
}
>实际测试部分 [Fact]
public void 验证取消优惠_退回使用次数_最大使用一次_允许取消一次()
{
XTaCodeDto codeDto = null;
验证取消优惠_初始化数据(ref codeDto, 1, 1); //第一次报名,取消
验证取消优惠_模拟报名使用邀请码_递增使用次数(1, codeDto);
验证取消优惠_退回使用次数_V1ForPrivate(1, codeDto);
//第一次取消会退回使用次数。
Assert.True(codeDto.UsedCount == 0 && codeDto.CancelUsedCount == 1); //第二次报名,取消
验证取消优惠_模拟报名使用邀请码_递增使用次数(1, codeDto);
验证取消优惠_退回使用次数_V1ForPrivate(1, codeDto);
//第二次取消后,超出允许取消次数限制,不会退回
Assert.True(codeDto.UsedCount == 1 && codeDto.CancelUsedCount == 2);
}
[Fact]
public void 验证取消优惠_退回使用次数_最大使用2次_允许取消两次()
{ XTaCodeDto codeDto = null;
验证取消优惠_初始化数据(ref codeDto, 2, 2); 验证取消优惠_模拟报名使用邀请码_递增使用次数(1, codeDto);
验证取消优惠_退回使用次数_V1ForPrivate(1, codeDto);
Assert.True(codeDto.UsedCount == 0 && codeDto.CancelUsedCount == 1); 验证取消优惠_模拟报名使用邀请码_递增使用次数(2, codeDto);
验证取消优惠_退回使用次数_V1ForPrivate(2, codeDto);
Assert.True(codeDto.UsedCount == 1 && codeDto.CancelUsedCount == 3); 验证取消优惠_模拟报名使用邀请码_递增使用次数(1, codeDto);
验证取消优惠_退回使用次数_V1ForPrivate(1, codeDto);
Assert.True(codeDto.UsedCount == 2 && codeDto.CancelUsedCount == 4);
}
使用单元测试的好处
快速验证结果,不用依赖各种数据库/缓存等环境。
代码指责更单一。
减少bug
方便后期持续集成
可参考连接
使用 dotnet test 和 xUnit 在 .NET Core 中进行 C# 单元测试
nsubstitute 介绍
Autofac介绍
单元测试的艺术
.Net单元测试业务实践的更多相关文章
- 【转】.NET(C#):浅谈程序集清单资源和RESX资源 关于单元测试的思考--Asp.Net Core单元测试最佳实践 封装自己的dapper lambda扩展-设计篇 编写自己的dapper lambda扩展-使用篇 正确理解CAP定理 Quartz.NET的使用(附源码) 整理自己的.net工具库 GC的前世与今生 Visual Studio Package 插件开发之自动生
[转].NET(C#):浅谈程序集清单资源和RESX资源 目录 程序集清单资源 RESX资源文件 使用ResourceReader和ResourceSet解析二进制资源文件 使用ResourceM ...
- STORM在线业务实践-集群空闲CPU飙高问题排查
源:http://daiwa.ninja/index.php/2015/07/18/storm-cpu-overload/ 2015-07-18AUTHORDAIWA STORM在线业务实践-集群空闲 ...
- 马蜂窝视频编辑框架设计及在 iOS 端的业务实践
(马蜂窝技术公众号原创内容,ID: mfwtech) 熟悉马蜂窝的朋友一定知道,点击马蜂窝 App 首页的发布按钮,会发现发布的内容已经被简化成「图文」或者「视频」. 长期以来,游记.问答.攻略等图文 ...
- Fetch方法封装、业务实践
说Fetch之前啊,我们不得不说一说Ajax了,以前使用最多的自然是jQuery封装的Ajax方法了,强大而且好用. 有人说了,jQuery的Ajax都已经封装得那么好了,你还整Fetch干什么,这不 ...
- ABAP单元测试最佳实践
本文包含了我在开发项目中经历过的实用的ABAP单元测试指导方针.我把它们安排成为问答的风格,欢迎任何人添加更多的Q&A's,以完成这个列表. 在我的项目中,只使用传统的ABAP report. ...
- VS单元测试入门实践教程
摘要:本教程不会介绍单元测试的基本理论知识,也不会和大家讨论在实际项目中是否需要写单元测试代码的问题.但是如果你此时想使用VS中的单元测试的工具来测试某个方法是否正确,可你又从来没真正实践过,那么本教 ...
- 关于单元测试的思考--Asp.Net Core单元测试最佳实践
在我们码字过程中,单元测试是必不可少的.但在从业过程中,很多开发者却对单元测试望而却步.有些时候并不是不想写,而是常常会碰到下面这些问题,让开发者放下了码字的脚步: 这个类初始数据太麻烦,你看:new ...
- VS2010单元测试入门实践教程
单元测试的重要性这里我就不多说了,以前大家一直使用NUnit来进行单元测试,其实早在Visual Studio 2005里面,微软就已经集成了一个叫Test的专门测试插件,经过几年的发展,这个工具现在 ...
- 使用gtest对DLL工程进行单元测试的实践
前言 关于单元测试的重要性.gtest的优缺点等就不说了.之前项目是没有做单元测试的,在VS的解决方案中,只有一个可执行的工程,其他的工程都是以DLL库的形式提供.本文只针对使用VS对DLL库进行单元 ...
随机推荐
- LeetCode题解之 Convert Sorted Array to Binary Search Tree
1.题目描述 2.问题分析 使用二分法即可. 3.代码 TreeNode* sortedArrayToBST(vector<int>& nums) { ) return NULL; ...
- Python数据类型之list和tuple
list是一种有序的集合,可以随时添加和删除其中的元素. 用len()函数可以获得list元素的个数. 用索引来访问list中每一个位置的元素,索引是从0开始的.如果要取最后一个元素,除了计算索引位置 ...
- 解决Hsqldb指针只能单向移动,不能回滚问题(.first())
Class.forName("org.hsqldb.jdbcDriver").newInstance(); Connection con = java.sql.DriverMana ...
- PowerShell发送邮件(587)
#定义邮件服务器 $smtpServer = "mail.xx.com" $smtpUser = "sender" $smtpPassword = " ...
- tidb导入大量数据报错:statement count 5001 exceeds the transaction limitation, autocommit = false
这是Tidb数据库事务提交数量达到上限的一种报错:因为tidb是分布式的数据库,tikv使用了底层的强一致性协议.这是分布式数据库必然遇到的一个问题,我们可以调整这个值:在tidb的配置文件里面“st ...
- ActiveX多线程回调JavaScript
http://www.cnblogs.com/zdxster/archive/2011/01/27/1945872.html
- hadoop集群为分布式搭建
1.准备Linux环境设置虚拟机网络 1.0点击VMware快捷方式,右键打开文件所在位置 -> 双击vmnetcfg.exe -> VMnet1 host-only ->修改 ...
- [技术] OIer的C++标准库 : STL入门
注: 本文主要摘取STL在OI中的常用技巧应用, 所以可能会重点说明容器部分和算法部分, 且不会讨论所有支持的函数/操作并主要讨论 C++11 前支持的特性. 如果需要详细完整的介绍请自行查阅标准文档 ...
- 带有function的JSON对象的序列化与还原
JSON对象的序列化与反序列化相信大家都很熟悉了.基本的api是JSON.parse与JSON.stringify. var json={ uiModule:'http://www.a.com', ...
- 函数式编程的终极形式:面向映射流的编程pipeline
1.单体(数据)映射:基本操作:数据的单次映射: 2.管道流:数据的流程化处理 基础是monand类型,形式是声明式编程: Pipeline模型: 它以一种“链式模型”来串接不同的程序或者不同的组件, ...