使用 Mock、Repository 和 UnitOfWork C# UnitTesting 测试 BLL
Testing BLL using Mock, Repository and UnitOfWork C# UnitTesting
我在我的项目中实现了 Repository 和 Unit Of Work,我使用的是相同的架构 here。所以,我的项目中有 3 层(DAL、BLL 和 UI) ,并且我将在我的 BLL 单元测试中使用 Mocking,但我对将它与此架构一起使用感到非常困惑,因为我有在 BLL 中使用的模型,这就是我需要测试。
注意:我看过一些题目,比如this, this and ,但实际上我没有找到符合我的案例,所以,如果你可以指导我完成这些以及如何在我的单元测试中使用 Mocking。
代码示例:DAL
IUnitOfWork:
public interface IUnitOfWork : IDisposable
{
IRepository<T> GetRepository<T>() where T : Entity;
int Save();
int SaveInDbTransaction(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted);
string ErrorMessage { get; }
}
IRepository:
public interface IRepository<TEntity> : IDisposable
{
IQueryable<TEntity> All(Expression<Func<TEntity, bool>> filter = null, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null, string includeProperties = "");
IEnumerable<TEntity> GetWithRawSql(string query, params object[] parameters);
IQueryable<TEntity> Filter(Expression<Func<TEntity, bool>> predicate);
IQueryable<TEntity> Filter<TKey>(Expression<Func<TEntity, bool>> filter, out int total, int index = 0, int size = 50);
bool Contains(Expression<Func<TEntity, bool>> predicate);
TEntity Find(params object[] keys);
TEntity Find(Expression<Func<TEntity, bool>> predicate);
void Create(TEntity entity);
void Delete(object entityId);
void Delete(TEntity entity);
void Delete(Expression<Func<TEntity, bool>> predicate);
void Update(TEntity entity);
int Count { get; }
}
工作单位:
public class UnitOfWork<TContext> : IUnitOfWork where TContext : DbContext
{
public string ErrorMessage { get; private set; }
private readonly TContext _context;
private bool _disposed;
private Dictionary<string, object> _repositories;
public UnitOfWork()
{
ErrorMessage = null;
_context = Activator.CreateInstance<TContext>();
_repositories = new Dictionary<string, object>();
}
public UnitOfWork(TContext context)
{
_context = context;
ErrorMessage = null;
}
public IRepository<TSet> GetRepository<TSet>() where TSet : Entity
{
if (_repositories == null)
{
_repositories = new Dictionary<string, object>();
}
if (_repositories.ContainsKey(typeof(TSet).Name))
{
return _repositories[typeof(TSet).Name] as IRepository<TSet>;
}
var repositoryInstance = new Repository<TSet, TContext>(_context);
_repositories.Add(typeof(TSet).Name, repositoryInstance);
return repositoryInstance;
}
public int Save()
{
try
{
#region Handling auditing
var modifiedEntries = _context.ChangeTracker.Entries()
.Where(x => x.Entity is IAuditableEntity
&& (x.State == EntityState.Added ||
x.State == EntityState.Modified));
foreach (var entry in modifiedEntries)
{
var entity = entry.Entity as IAuditableEntity;
if (entity != null)
{
var identityName = Thread.CurrentPrincipal.Identity.Name;
var now = DateTime.UtcNow;
if (entry.State == EntityState.Added)
{
entity.CreatedBy = identityName;
entity.Created = now;
}
else
{
_context.Entry(entity).Property(x => x.CreatedBy).IsModified = false;
_context.Entry(entity).Property(x => x.Created).IsModified = false;
}
entity.ModifiedBy = identityName;
entity.Modified = now;
}
}
#endregion
var affectedRows = _context.SaveChanges();
return affectedRows;
}
catch (DbEntityValidationException dbEx)
{
foreach (var validationError in dbEx.EntityValidationErrors.SelectMany(
validationErrors => validationErrors.ValidationErrors))
{
ErrorMessage += $"Property: {validationError.PropertyName} Error: {validationError.ErrorMessage}" +
Environment.NewLine;
}
throw new Exception(ErrorMessage, dbEx);
}
catch (Exception exception)
{
ErrorMessage = exception.Message;
throw new Exception(ErrorMessage, exception);
}
}
public int SaveInDbTransaction(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted)
{
DbContextTransaction transaction = null;
try
{
transaction = _context.Database.BeginTransaction(IsolationLevel.ReadCommitted);
using (transaction)
{
#region Handling auditing
var modifiedEntries = _context.ChangeTracker.Entries()
.Where(x => x.Entity is IAuditableEntity
&& (x.State == EntityState.Added ||
x.State == EntityState.Modified));
foreach (var entry in modifiedEntries)
{
var entity = entry.Entity as IAuditableEntity;
if (entity != null)
{
var identityName = Thread.CurrentPrincipal.Identity.Name;
var now = DateTime.UtcNow;
if (entry.State == EntityState.Added)
{
entity.CreatedBy = identityName;
entity.Created = now;
}
else
{
_context.Entry(entity).Property(x => x.CreatedBy).IsModified = false;
_context.Entry(entity).Property(x => x.Created).IsModified = false;
}
entity.ModifiedBy = identityName;
entity.Modified = now;
}
}
#endregion
var affectedRows = _context.SaveChanges();
transaction.Commit();
return affectedRows;
}
}
catch (DbEntityValidationException dbEx)
{
foreach (var validationError in dbEx.EntityValidationErrors.SelectMany(
validationErrors => validationErrors.ValidationErrors))
{
ErrorMessage += $"Property: {validationError.PropertyName} Error: {validationError.ErrorMessage}" +
Environment.NewLine;
}
transaction?.Rollback();
throw new Exception(ErrorMessage, dbEx);
}
catch (Exception exception)
{
ErrorMessage = exception.Message;
transaction?.Rollback();
throw new Exception(ErrorMessage, exception);
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_context.Dispose();
}
}
_disposed = true;
}
}
存储库:
public class Repository<TEntity, TContext> : IRepository<TEntity>
where TEntity : Entity
where TContext : DbContext
{
private readonly TContext _context;
protected DbSet<TEntity> DbSet => _context.Set<TEntity>();
public Repository(TContext session)
{
_context = session;
}
public void Dispose()
{
_context?.Dispose();
}
public IQueryable<TEntity> All(Expression<Func<TEntity, bool>> filter = null, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null, string includeProperties = "")
{
//return DbSet.AsQueryable();
var query = DbSet.AsQueryable();
if (filter != null)
{
query = query.Where(filter);
}
query = includeProperties.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries)
.Aggregate(query, (current, includeProperty) => current.Include(includeProperty));
return orderBy?.Invoke(query).AsQueryable() ?? query.AsQueryable();
}
public IEnumerable<TEntity> GetWithRawSql(string query, params object[] parameters)
{
return DbSet.SqlQuery(query, parameters).ToList();
}
public IQueryable<TEntity> Filter(Expression<Func<TEntity, bool>> predicate)
{
return DbSet.Where(predicate).AsQueryable();
}
public IQueryable<TEntity> Filter<TKey>(Expression<Func<TEntity, bool>> predicate, out int total, int index = 0,
int size = 50)
{
var result = DbSet.Where(predicate);
total = result.Count();
return result.Skip(index).Take(size);
}
public bool Contains(Expression<Func<TEntity, bool>> predicate)
{
return DbSet.Count(predicate) > 0;
}
public TEntity Find(params object[] keys)
{
return DbSet.Find(keys);
}
public TEntity Find(Expression<Func<TEntity, bool>> predicate)
{
return DbSet.FirstOrDefault(predicate);
}
public void Create(TEntity entity)
{
DbSet.Add(entity);
}
public void Delete(object entityId)
{
var entity = DbSet.Find(entityId);
if (entity != null)
{
DbSet.Remove(entity);
}
}
public void Delete(TEntity entity)
{
DbSet.Remove(entity);
}
public void Delete(Expression<Func<TEntity, bool>> predicate)
{
var objects = Filter(predicate);
foreach (var obj in objects)
DbSet.Remove(obj);
}
public void Update(TEntity entity)
{
var entry = _context.Entry(entity);
DbSet.Attach(entity);
entry.State = EntityState.Modified;
}
public int Count => DbSet.Count();
}
BLL:
在 BLL 中,我有一个模型供 DAL 中的每个实体与 UI 层通信,并且有一个扩展方法使用 AutoMapper[= 从实体转换为模型,反之亦然47=],我为每个模型都有一个 class,其中包含我需要用该实体实现的所有逻辑,这里是 BLL class 的示例,我需要用 模拟:
public class ClientManager
{
public int Add(ClientModel model)
{
var entity = model.ToEntity();
using (var uow = new UnitOfWork<SubscriptionContext>())
{
if (model.IsValid())
{
var entityRepository = uow.GetRepository<Data.Entities.Client>();
entityRepository.Create(entity);
var affected = uow.Save();
if (affected < 1)
{
throw new Exception(uow.ErrorMessage);
}
Logger.Log(Logger.LogLevel.Information, this.GetType().FullName, MethodBase.GetCurrentMethod(), "Adding new entity: " + entity.Id, null, Thread.CurrentPrincipal.Identity.Name);
return affected;
}
else
{
throw new Exception("Model is not valid.");
}
}
}
public int Update(ClientModel model)
{
var entity = model.ToEntity();
using (var uow = new UnitOfWork<SubscriptionContext>())
{
if (model.IsValid())
{
var entityRepository = uow.GetRepository<Data.Entities.Client>();
entityRepository.Update(entity);
var affected = uow.Save();
if (affected < 1)
{
throw new Exception(uow.ErrorMessage);
}
Logger.Log(Logger.LogLevel.Information, this.GetType().FullName, MethodBase.GetCurrentMethod(), "Updating existing entity: " + entity.Id, null, Thread.CurrentPrincipal.Identity.Name);
return affected;
}
else
{
throw new Exception("Model is not valid.");
}
}
}
public int Delete(int entityId)
{
using (var uow = new UnitOfWork<SubscriptionContext>())
{
if (entityId > 0)
{
var entityRepository = uow.GetRepository<Data.Entities.Client>();
entityRepository.Delete(entityId);
var affected = uow.Save();
if (affected < 1)
{
throw new Exception(uow.ErrorMessage);
}
Logger.Log(Logger.LogLevel.Information, this.GetType().FullName, MethodBase.GetCurrentMethod(), "Removing existing entity: " + entityId, null, Thread.CurrentPrincipal.Identity.Name);
return affected;
}
else
{
throw new Exception("There is no data to delete at the current position.");
}
}
}
public ClientModel Find(int entityId)
{
using (var uow = new UnitOfWork<SubscriptionContext>())
{
if (entityId > 0)
{
var entityRepository = uow.GetRepository<Data.Entities.Client>();
var entity = entityRepository.Find(entityId);
if(entity != null) {
return entity.ToModel();
}
}
throw new Exception("There is no data to delete at the current position.");
}
}
}
您想模拟,但您似乎没有使用任何依赖注入。相反,您只是在需要的地方创建自己的 UnitOfWork<SubscriptionContext>
实现。
我建议您研究依赖注入,并实际注册一个 UnitOfWorkFactory
以插入到您的 ClientManager
.
您的代码将如下所示:
public class ClientManager
{
private readonly IUnitOfWorkFactory UowFactory;
public ClientManager(IUnitOfWorkFactory<SubscriptionContext> uowFactory)
{
UowFactory = uowFactory;
}
public int Add(ClientModel model)
{
var entity = model.ToEntity();
using (var uow = uowFactory.GetUoW())
{
// dowork
}
}
}
您可以阅读依赖注入(例如使用 unity)和在线工厂模式,例如 here
现在在您的单元测试中,您可以简单地使用您自己的 IUnitOfWorkFactory
实现,在其中您 return 模拟 UoW,如下所示:
var UowMock = new Mock<IUnitOfWork<SubscriptionContext>();
var UowFactoryMock = new Mock<IUowFactory>();
UowFactoryMock.Stub(f => f.GetUoW()).Returns(UowMock);
var clientManager = new ClientManager(UowFactoryMock);
// Test whatever you want in your clientManager!
当然,在调用方法时,您可能必须将工作单元设置为 return 预期值。如何做到这一点完全取决于您的测试框架。
我在我的项目中实现了 Repository 和 Unit Of Work,我使用的是相同的架构 here。所以,我的项目中有 3 层(DAL、BLL 和 UI) ,并且我将在我的 BLL 单元测试中使用 Mocking,但我对将它与此架构一起使用感到非常困惑,因为我有在 BLL 中使用的模型,这就是我需要测试。
注意:我看过一些题目,比如this, this and
代码示例:DAL
IUnitOfWork:
public interface IUnitOfWork : IDisposable
{
IRepository<T> GetRepository<T>() where T : Entity;
int Save();
int SaveInDbTransaction(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted);
string ErrorMessage { get; }
}
IRepository:
public interface IRepository<TEntity> : IDisposable
{
IQueryable<TEntity> All(Expression<Func<TEntity, bool>> filter = null, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null, string includeProperties = "");
IEnumerable<TEntity> GetWithRawSql(string query, params object[] parameters);
IQueryable<TEntity> Filter(Expression<Func<TEntity, bool>> predicate);
IQueryable<TEntity> Filter<TKey>(Expression<Func<TEntity, bool>> filter, out int total, int index = 0, int size = 50);
bool Contains(Expression<Func<TEntity, bool>> predicate);
TEntity Find(params object[] keys);
TEntity Find(Expression<Func<TEntity, bool>> predicate);
void Create(TEntity entity);
void Delete(object entityId);
void Delete(TEntity entity);
void Delete(Expression<Func<TEntity, bool>> predicate);
void Update(TEntity entity);
int Count { get; }
}
工作单位:
public class UnitOfWork<TContext> : IUnitOfWork where TContext : DbContext
{
public string ErrorMessage { get; private set; }
private readonly TContext _context;
private bool _disposed;
private Dictionary<string, object> _repositories;
public UnitOfWork()
{
ErrorMessage = null;
_context = Activator.CreateInstance<TContext>();
_repositories = new Dictionary<string, object>();
}
public UnitOfWork(TContext context)
{
_context = context;
ErrorMessage = null;
}
public IRepository<TSet> GetRepository<TSet>() where TSet : Entity
{
if (_repositories == null)
{
_repositories = new Dictionary<string, object>();
}
if (_repositories.ContainsKey(typeof(TSet).Name))
{
return _repositories[typeof(TSet).Name] as IRepository<TSet>;
}
var repositoryInstance = new Repository<TSet, TContext>(_context);
_repositories.Add(typeof(TSet).Name, repositoryInstance);
return repositoryInstance;
}
public int Save()
{
try
{
#region Handling auditing
var modifiedEntries = _context.ChangeTracker.Entries()
.Where(x => x.Entity is IAuditableEntity
&& (x.State == EntityState.Added ||
x.State == EntityState.Modified));
foreach (var entry in modifiedEntries)
{
var entity = entry.Entity as IAuditableEntity;
if (entity != null)
{
var identityName = Thread.CurrentPrincipal.Identity.Name;
var now = DateTime.UtcNow;
if (entry.State == EntityState.Added)
{
entity.CreatedBy = identityName;
entity.Created = now;
}
else
{
_context.Entry(entity).Property(x => x.CreatedBy).IsModified = false;
_context.Entry(entity).Property(x => x.Created).IsModified = false;
}
entity.ModifiedBy = identityName;
entity.Modified = now;
}
}
#endregion
var affectedRows = _context.SaveChanges();
return affectedRows;
}
catch (DbEntityValidationException dbEx)
{
foreach (var validationError in dbEx.EntityValidationErrors.SelectMany(
validationErrors => validationErrors.ValidationErrors))
{
ErrorMessage += $"Property: {validationError.PropertyName} Error: {validationError.ErrorMessage}" +
Environment.NewLine;
}
throw new Exception(ErrorMessage, dbEx);
}
catch (Exception exception)
{
ErrorMessage = exception.Message;
throw new Exception(ErrorMessage, exception);
}
}
public int SaveInDbTransaction(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted)
{
DbContextTransaction transaction = null;
try
{
transaction = _context.Database.BeginTransaction(IsolationLevel.ReadCommitted);
using (transaction)
{
#region Handling auditing
var modifiedEntries = _context.ChangeTracker.Entries()
.Where(x => x.Entity is IAuditableEntity
&& (x.State == EntityState.Added ||
x.State == EntityState.Modified));
foreach (var entry in modifiedEntries)
{
var entity = entry.Entity as IAuditableEntity;
if (entity != null)
{
var identityName = Thread.CurrentPrincipal.Identity.Name;
var now = DateTime.UtcNow;
if (entry.State == EntityState.Added)
{
entity.CreatedBy = identityName;
entity.Created = now;
}
else
{
_context.Entry(entity).Property(x => x.CreatedBy).IsModified = false;
_context.Entry(entity).Property(x => x.Created).IsModified = false;
}
entity.ModifiedBy = identityName;
entity.Modified = now;
}
}
#endregion
var affectedRows = _context.SaveChanges();
transaction.Commit();
return affectedRows;
}
}
catch (DbEntityValidationException dbEx)
{
foreach (var validationError in dbEx.EntityValidationErrors.SelectMany(
validationErrors => validationErrors.ValidationErrors))
{
ErrorMessage += $"Property: {validationError.PropertyName} Error: {validationError.ErrorMessage}" +
Environment.NewLine;
}
transaction?.Rollback();
throw new Exception(ErrorMessage, dbEx);
}
catch (Exception exception)
{
ErrorMessage = exception.Message;
transaction?.Rollback();
throw new Exception(ErrorMessage, exception);
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_context.Dispose();
}
}
_disposed = true;
}
}
存储库:
public class Repository<TEntity, TContext> : IRepository<TEntity>
where TEntity : Entity
where TContext : DbContext
{
private readonly TContext _context;
protected DbSet<TEntity> DbSet => _context.Set<TEntity>();
public Repository(TContext session)
{
_context = session;
}
public void Dispose()
{
_context?.Dispose();
}
public IQueryable<TEntity> All(Expression<Func<TEntity, bool>> filter = null, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null, string includeProperties = "")
{
//return DbSet.AsQueryable();
var query = DbSet.AsQueryable();
if (filter != null)
{
query = query.Where(filter);
}
query = includeProperties.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries)
.Aggregate(query, (current, includeProperty) => current.Include(includeProperty));
return orderBy?.Invoke(query).AsQueryable() ?? query.AsQueryable();
}
public IEnumerable<TEntity> GetWithRawSql(string query, params object[] parameters)
{
return DbSet.SqlQuery(query, parameters).ToList();
}
public IQueryable<TEntity> Filter(Expression<Func<TEntity, bool>> predicate)
{
return DbSet.Where(predicate).AsQueryable();
}
public IQueryable<TEntity> Filter<TKey>(Expression<Func<TEntity, bool>> predicate, out int total, int index = 0,
int size = 50)
{
var result = DbSet.Where(predicate);
total = result.Count();
return result.Skip(index).Take(size);
}
public bool Contains(Expression<Func<TEntity, bool>> predicate)
{
return DbSet.Count(predicate) > 0;
}
public TEntity Find(params object[] keys)
{
return DbSet.Find(keys);
}
public TEntity Find(Expression<Func<TEntity, bool>> predicate)
{
return DbSet.FirstOrDefault(predicate);
}
public void Create(TEntity entity)
{
DbSet.Add(entity);
}
public void Delete(object entityId)
{
var entity = DbSet.Find(entityId);
if (entity != null)
{
DbSet.Remove(entity);
}
}
public void Delete(TEntity entity)
{
DbSet.Remove(entity);
}
public void Delete(Expression<Func<TEntity, bool>> predicate)
{
var objects = Filter(predicate);
foreach (var obj in objects)
DbSet.Remove(obj);
}
public void Update(TEntity entity)
{
var entry = _context.Entry(entity);
DbSet.Attach(entity);
entry.State = EntityState.Modified;
}
public int Count => DbSet.Count();
}
BLL:
在 BLL 中,我有一个模型供 DAL 中的每个实体与 UI 层通信,并且有一个扩展方法使用 AutoMapper[= 从实体转换为模型,反之亦然47=],我为每个模型都有一个 class,其中包含我需要用该实体实现的所有逻辑,这里是 BLL class 的示例,我需要用 模拟:
public class ClientManager
{
public int Add(ClientModel model)
{
var entity = model.ToEntity();
using (var uow = new UnitOfWork<SubscriptionContext>())
{
if (model.IsValid())
{
var entityRepository = uow.GetRepository<Data.Entities.Client>();
entityRepository.Create(entity);
var affected = uow.Save();
if (affected < 1)
{
throw new Exception(uow.ErrorMessage);
}
Logger.Log(Logger.LogLevel.Information, this.GetType().FullName, MethodBase.GetCurrentMethod(), "Adding new entity: " + entity.Id, null, Thread.CurrentPrincipal.Identity.Name);
return affected;
}
else
{
throw new Exception("Model is not valid.");
}
}
}
public int Update(ClientModel model)
{
var entity = model.ToEntity();
using (var uow = new UnitOfWork<SubscriptionContext>())
{
if (model.IsValid())
{
var entityRepository = uow.GetRepository<Data.Entities.Client>();
entityRepository.Update(entity);
var affected = uow.Save();
if (affected < 1)
{
throw new Exception(uow.ErrorMessage);
}
Logger.Log(Logger.LogLevel.Information, this.GetType().FullName, MethodBase.GetCurrentMethod(), "Updating existing entity: " + entity.Id, null, Thread.CurrentPrincipal.Identity.Name);
return affected;
}
else
{
throw new Exception("Model is not valid.");
}
}
}
public int Delete(int entityId)
{
using (var uow = new UnitOfWork<SubscriptionContext>())
{
if (entityId > 0)
{
var entityRepository = uow.GetRepository<Data.Entities.Client>();
entityRepository.Delete(entityId);
var affected = uow.Save();
if (affected < 1)
{
throw new Exception(uow.ErrorMessage);
}
Logger.Log(Logger.LogLevel.Information, this.GetType().FullName, MethodBase.GetCurrentMethod(), "Removing existing entity: " + entityId, null, Thread.CurrentPrincipal.Identity.Name);
return affected;
}
else
{
throw new Exception("There is no data to delete at the current position.");
}
}
}
public ClientModel Find(int entityId)
{
using (var uow = new UnitOfWork<SubscriptionContext>())
{
if (entityId > 0)
{
var entityRepository = uow.GetRepository<Data.Entities.Client>();
var entity = entityRepository.Find(entityId);
if(entity != null) {
return entity.ToModel();
}
}
throw new Exception("There is no data to delete at the current position.");
}
}
}
您想模拟,但您似乎没有使用任何依赖注入。相反,您只是在需要的地方创建自己的 UnitOfWork<SubscriptionContext>
实现。
我建议您研究依赖注入,并实际注册一个 UnitOfWorkFactory
以插入到您的 ClientManager
.
您的代码将如下所示:
public class ClientManager
{
private readonly IUnitOfWorkFactory UowFactory;
public ClientManager(IUnitOfWorkFactory<SubscriptionContext> uowFactory)
{
UowFactory = uowFactory;
}
public int Add(ClientModel model)
{
var entity = model.ToEntity();
using (var uow = uowFactory.GetUoW())
{
// dowork
}
}
}
您可以阅读依赖注入(例如使用 unity)和在线工厂模式,例如 here
现在在您的单元测试中,您可以简单地使用您自己的 IUnitOfWorkFactory
实现,在其中您 return 模拟 UoW,如下所示:
var UowMock = new Mock<IUnitOfWork<SubscriptionContext>();
var UowFactoryMock = new Mock<IUowFactory>();
UowFactoryMock.Stub(f => f.GetUoW()).Returns(UowMock);
var clientManager = new ClientManager(UowFactoryMock);
// Test whatever you want in your clientManager!
当然,在调用方法时,您可能必须将工作单元设置为 return 预期值。如何做到这一点完全取决于您的测试框架。