模拟 DbContext - 无法插入新对象

4

https://msdn.microsoft.com/zh-cn/data/dn314429.aspx

我正在尝试使用类似于上面链接中的查询场景的代码。 EF 6.1 + Moq。 如果数据是预先填充好的,它可以正常工作(我可以无问题地进行查询):

_context = new Mock<MyContext>();

IQueryable<users> users = new List<users>
        {
            new users{id = 1, email = "test@test.pl", password = "test", created = DateTime.Now, modified = DateTime.Now},
            new users{id = 2, email = "test2@test.pl", password = "test", created = DateTime.Now, modified = DateTime.Now},
        }.AsQueryable();
var mockUsers = new Mock<DbSet<users>>();
        mockUsers.As<IQueryable<users>>().Setup(m => m.Provider).Returns(users.Provider);
        mockUsers.As<IQueryable<users>>().Setup(m => m.Expression).Returns(users.Expression);
        mockUsers.As<IQueryable<users>>().Setup(m => m.ElementType).Returns(users.ElementType);
        mockUsers.As<IQueryable<users>>().Setup(m => m.GetEnumerator()).Returns(users.GetEnumerator());

_context.Setup(m => m.users).Returns(mockUsers.Object);

但是当我尝试在我的测试方法中添加新对象时,它不起作用:
[TestMethod]
    public void AddUser()
    {
        _context.Object.users.Add(new users { email = "test3@test.pl", password = "test", created = DateTime.Now, modified = DateTime.Now });
        _context.Object.SaveChanges();
        var count = _context.Object.users.Count();
        Assert.AreEqual(3, count); // count == 2 instead 3
    }

你能否修改它,使我的模拟上下文实际应用SaveChanges?我的想法是否正确,或者这种方法之所以错误是因为某些原因?

如果我使用非查询方法,我将无法使用以下内容:

mockContext.Object.users.Count()

假设我有一个测试方法,其中包含以下片段:
 context.cards.Add(card);
 context.SaveChanges();
 SetCardForUser(card.id, user);

 protected void SetCardForUser(long cardId, users user)
    {
        user.card_id = cardId;
        context.SaveChanges();
    }

我需要在向数据库插入对象后返回card.id。

我应该严格遵循SRP原则,只编写一个方法来处理单一事务吗?

如果只有AddCardAndStartAuthCharge是公共方法,其他方法是受保护的,我该如何测试这个方法?

public async Task<long> AddCardAndStartAuthCharge(CardModel model, string username)
    {
        var hash = await GetHashForCardAuth(model);
        var CardWithId = AddCardToDatabase(model, username);
        long chargeId = AddHashToCard(CardWithId, hash);
        return chargeId;
    }

如果你看一下,链接中有一个“添加”场景:测试非查询场景 - xanatos
我编辑了我的问题以更好地解释我的问题。 - Adiqq
我认为这里的主要问题是您说需要修改卡片 ID 并在保存后检索对象的 ID。ID 由 EF 处理,在我的经验中,没有必要自己修改或使用相应的属性。为什么您需要那样做呢? - hschne
这与我的另一个问题有关:http://stackoverflow.com/questions/28901691/inserting-two-entites-with-foreign-keys-in-one-turn - Adiqq
3个回答

0

是的,可以模拟保存方法。请看这个:

IQualityIssuesRepository qir = new QualityIssuesRepository();
        Mock<IQualityIssuesRepository> qualityIssuesRepository = new Mock<IQualityIssuesRepository>();
qualityIssuesRepository.CallBase = true;
Action<List<int>, int> saveNotification = qir.SaveNotification;
qualityIssuesRepository.Setup(p => p.SaveNotification(It.IsAny<List<int>>(), It.IsAny<int>()))
            .Callback(saveNotification);

您应该在模拟存储库中设置Save方法。只需创建该存储库的实例,并将该方法分配给模拟存储库即可。希望有所帮助 :)

0
考虑在比你使用的工具更高的抽象层次上进行模拟。
也许你的视图模型应该依赖于服务,而不是你使用的工具的细节(例如IIsesServiceChannel)。
以下是一个例子:
我通常使用IServices、Services和MockServices。
- IServices提供了所有业务逻辑必须调用方法的可用操作。 - Services是数据访问层,我的代码后端将其注入到视图模型中(即实际数据库)。 - MockServices是数据访问层,我的单元测试将其注入到视图模型中(即模拟数据)。
IServices:
public interface IServices
{
    IEnumerable<Warehouse> LoadSupply(Lookup lookup);
    IEnumerable<Demand> LoadDemand(IEnumerable<string> stockCodes, int daysFilter, Lookup lookup);

    IEnumerable<Inventory> LoadParts(int daysFilter);
    Narration LoadNarration(string stockCode);
    IEnumerable<PurchaseHistory> LoadPurchaseHistory(string stockCode);

    IEnumerable<StockAlternative> LoadAlternativeStockCodes();
    AdditionalInfo GetSupplier(string stockCode);
}

模拟服务:

public class MockServices : IServices
{
    #region Constants
    const int DEFAULT_TIMELINE = 30;
    #endregion

    #region Singleton
    static MockServices _mockServices = null;

    private MockServices()
    {
    }

    public static MockServices Instance
    {
        get
        {
            if (_mockServices == null)
            {
                _mockServices = new MockServices();
            }

            return _mockServices;
        }
    }
    #endregion

    #region Members
    IEnumerable<Warehouse> _supply = null;
    IEnumerable<Demand> _demand = null;
    IEnumerable<StockAlternative> _stockAlternatives = null;
    IConfirmationInteraction _refreshConfirmationDialog = null;
    IConfirmationInteraction _extendedTimelineConfirmationDialog = null;
    #endregion

    #region Boot
    public MockServices(IEnumerable<Warehouse> supply, IEnumerable<Demand> demand, IEnumerable<StockAlternative> stockAlternatives, IConfirmationInteraction refreshConfirmationDialog, IConfirmationInteraction extendedTimelineConfirmationDialog)
    {
        _supply = supply;
        _demand = demand;
        _stockAlternatives = stockAlternatives;
        _refreshConfirmationDialog = refreshConfirmationDialog;
        _extendedTimelineConfirmationDialog = extendedTimelineConfirmationDialog;
    }

    public IEnumerable<StockAlternative> LoadAlternativeStockCodes()
    {
        return _stockAlternatives;
    }

    public IEnumerable<Warehouse> LoadSupply(Lookup lookup)
    {
        return _supply;
    }

    public IEnumerable<Demand> LoadDemand(IEnumerable<string> stockCodes, int daysFilter, Syspro.Business.Lookup lookup)
    {
        return _demand;
    }

    public IEnumerable<Inventory> LoadParts(int daysFilter)
    {
        var job1 = new Job() { Id = Globals.jobId1, AssembledRequiredDate = DateTime.Now, StockCode = Globals.stockCode100 };
        var job2 = new Job() { Id = Globals.jobId2, AssembledRequiredDate = DateTime.Now, StockCode = Globals.stockCode200 };
        var job3 = new Job() { Id = Globals.jobId3, AssembledRequiredDate = DateTime.Now, StockCode = Globals.stockCode300 };

        return new HashSet<Inventory>()
        {
            new Inventory() { StockCode = Globals.stockCode100, UnitQTYRequired = 1, Category = "Category_1", Details = new PartDetails() { Warehouse = Globals.Instance.warehouse1, Job = job1} },
            new Inventory() { StockCode = Globals.stockCode200, UnitQTYRequired = 2, Category = "Category_1", Details = new PartDetails() { Warehouse = Globals.Instance.warehouse1, Job = job2} },
            new Inventory() { StockCode = Globals.stockCode300, UnitQTYRequired = 3, Category = "Category_1", Details = new PartDetails() { Warehouse = Globals.Instance.warehouse1, Job = job3} },
        };
    }
    #endregion

    #region Selection
    public Narration LoadNarration(string stockCode)
    {
        return new Narration()
        {
            Text = "Some description"
        };
    }

    public IEnumerable<PurchaseHistory> LoadPurchaseHistory(string stockCode)
    {
        return new List<PurchaseHistory>();
    }

    public AdditionalInfo GetSupplier(string stockCode)
    {
        return new AdditionalInfo()
        {
            SupplierName = "Some supplier name"
        };
    }
    #endregion

    #region Creation
    public Inject Dependencies(IEnumerable<Warehouse> supply, IEnumerable<Demand> demand, IEnumerable<StockAlternative> stockAlternatives, IConfirmationInteraction refreshConfirmation = null, IConfirmationInteraction extendedTimelineConfirmation = null)
    {
        return new Inject()
        {
            Services = new MockServices(supply, demand, stockAlternatives, refreshConfirmation, extendedTimelineConfirmation),

            Lookup = new Lookup()
            {
                PartKeyToCachedParts = new Dictionary<string, Inventory>(),
                PartkeyToStockcode = new Dictionary<string, string>(),
                DaysRemainingToCompletedJobs = new Dictionary<int, HashSet<Job>>(),
.
.
.

            },

            DaysFilterDefault = DEFAULT_TIMELINE,
            FilterOnShortage = true,
            PartCache = null
        };
    }

    public List<StockAlternative> Alternatives()
    {
        var stockAlternatives = new List<StockAlternative>() { new StockAlternative() { StockCode = Globals.stockCode100, AlternativeStockcode = Globals.stockCode100Alt1 } };
        return stockAlternatives;
    }

    public List<Demand> Demand()
    {
        var demand = new List<Demand>()
        {
            new Demand(){ Job = new Job{ Id = Globals.jobId1, StockCode = Globals.stockCode100, AssembledRequiredDate = DateTime.Now}, StockCode = Globals.stockCode100, RequiredQTY = 1}, 
            new Demand(){ Job = new Job{ Id = Globals.jobId2, StockCode = Globals.stockCode200, AssembledRequiredDate = DateTime.Now}, StockCode = Globals.stockCode200, RequiredQTY = 2}, 
        };
        return demand;
    }

    public List<Warehouse> Supply()
    {
        var supply = new List<Warehouse>() 
        { 
            Globals.Instance.warehouse1, 
            Globals.Instance.warehouse2, 
            Globals.Instance.warehouse3,
        };
        return supply;
    }
    #endregion
}

服务:

public class Services : IServices
{
    #region Singleton
    static Services services = null;

    private Services()
    {
    }

    public static Services Instance
    {
        get
        {
            if (services == null)
            {
                services = new Services();
            }

            return services;
        }
    }
    #endregion

    public IEnumerable<Inventory> LoadParts(int daysFilter)
    {
        return InventoryRepository.Instance.Get(daysFilter);
    }

    public IEnumerable<Warehouse> LoadSupply(Lookup lookup)
    {
        return SupplyRepository.Instance.Get(lookup);
    }

    public IEnumerable<StockAlternative> LoadAlternativeStockCodes()
    {
        return InventoryRepository.Instance.GetAlternatives();
    }

    public IEnumerable<Demand> LoadDemand(IEnumerable<string> stockCodes, int daysFilter, Lookup lookup)
    {
        return DemandRepository.Instance.Get(stockCodes, daysFilter, lookup);
    }
.
.
.

单元测试:

    [TestMethod]
    public void shortage_exists()
    {
        // Setup
        var supply = new List<Warehouse>() { Globals.Instance.warehouse1, Globals.Instance.warehouse2, Globals.Instance.warehouse3 };
        Globals.Instance.warehouse1.TotalQty = 1;
        Globals.Instance.warehouse2.TotalQty = 2;
        Globals.Instance.warehouse3.TotalQty = 3;

        var demand = new List<Demand>()
        {
            new Demand(){ Job = new Job{ Id = Globals.jobId1, StockCode = Globals.stockCode100, AssembledRequiredDate = DateTime.Now}, StockCode = Globals.stockCode100, RequiredQTY = 1}, 
            new Demand(){ Job = new Job{ Id = Globals.jobId2, StockCode = Globals.stockCode200, AssembledRequiredDate = DateTime.Now}, StockCode = Globals.stockCode200, RequiredQTY = 3}, 
            new Demand(){ Job = new Job{ Id = Globals.jobId3, StockCode = Globals.stockCode300, AssembledRequiredDate = DateTime.Now}, StockCode = Globals.stockCode300, RequiredQTY = 4}, 
        };

        var alternatives = _mock.Alternatives();
        var dependencies = _mock.Dependencies(supply, demand, alternatives);

        var viewModel = new MainViewModel();
        viewModel.Register(dependencies);

        // Test
        viewModel.Load();

        AwaitCompletion(viewModel);

        // Verify
        var part100IsNotShort = dependencies.PartCache.Where(p => (p.StockCode == Globals.stockCode100) && (!p.HasShortage)).Single() != null;
        var part200IsShort = dependencies.PartCache.Where(p => (p.StockCode == Globals.stockCode200) && (p.HasShortage)).Single() != null;
        var part300IsShort = dependencies.PartCache.Where(p => (p.StockCode == Globals.stockCode300) && (p.HasShortage)).Single() != null;

        Assert.AreEqual(true, part100IsNotShort &&
                                part200IsShort &&
                                part300IsShort);
    }

CodeBehind:

    public MainWindow()
    {
        InitializeComponent();

        this.Loaded += (s, e) =>
            {
                this.viewModel = this.DataContext as MainViewModel;

                var dependencies = GetDependencies();
                this.viewModel.Register(dependencies);
.
.
.

视图模型:

    public MyViewModel()
    {
.
.
.
    public void Register(Inject dependencies)
    {
        try
        {
            this.Injected = dependencies;

            this.Injected.RefreshConfirmation.RequestConfirmation += (message, caption) =>
                {
                    var result = MessageBox.Show(message, caption, MessageBoxButton.YesNo, MessageBoxImage.Question);
                    return result;
                };

            this.Injected.ExtendTimelineConfirmation.RequestConfirmation += (message, caption) =>
                {
                    var result = MessageBox.Show(message, caption, MessageBoxButton.YesNo, MessageBoxImage.Question);
                    return result;
                };

.
.
.
        }

        catch (Exception ex)
        {
            Debug.WriteLine(ex.GetBaseException().Message);
        }
    }

0

正如在评论中提到的那样,在使用EntityFramework时,绝对没有必要自己管理ID。请观察此代码片段:

MyContext context = new MyContext();
User user = new User() { Name = "Name1" };
context.Users.Add(user);
context.SaveChanges();
Console.WriteLine(user.Id);

执行SaveChanges后会自动分配一个ID。因此,像您其他问题中的代码片段可以很容易地进行重构。

//Old
public CardModel AddCard(CardModel model, string username)
{
    var user = context.users.Where(x => x.email.ToLower() == username.ToLower()).First();
    var card = new cards()
    {
        created = DateTime.Now,
        modified = DateTime.Now,
        name = model.Name,
        type = model.CardType,
        user_id = user.id
    };
    context.cards.Add(card);
    context.SaveChanges();
    model.Id = card.id;
    return model;
}

//New
public CardModel AddCard(CardModel model, string username)
{
    var user = context.users.Where(x => x.email.ToLower() == username.ToLower()).First();
    user.Cards.Add(model)
    user.SaveOrUpdate()
    return model;
}

请注意,在任何时候都不需要类似 model.Id = card.id 的操作!
这可能需要对您现有的模型进行一些更改才能正常工作。您将需要配置 EntityFramework,以正确地将模型中的更改级联到所有子模型(例如,如果删除一个 User,则删除所有 Cards)。
现在,回答您的问题。如果您按照上述方式重构代码,您将不再依赖于 SaveChanges() 和上下文中包含的 DBSet 来提供您的模型。
由于您正在使用模拟对象,因此 ID 也不会改变,但鉴于您的代码,您不应该明确使用任何 ID。
您已经拥有的模拟对象就足够了,因为您永远不必从上下文中检索刚刚添加到该上下文中的模型才能使用它。牢记这一点,既不需要模拟 SaveChanges 的后果,也不需要模拟诸如 Count 之类的事物。

如果我不使用Automapper或实体直接操作,那么在这种情况下是否可以使用user.Cards.Add(model)?通常我会像CardModel这样使用视图模型和cards这样的实体,并手动映射它们或使用automapper/linq selects。 - Adiqq
@Adiqq 嗯,这真的取决于你具体的情况。目前来看:不行。 - hschne

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接