問題描述
我正在單元測試,看看是否調用了一個方法.
I'm unit testing to see if a method is called.
[Fact]
public void Can_Save_Project_Changes()
{
//Arrange
var user = new AppUser() { UserName = "JohnDoe", Id = "1" };
Mock<IRepository> mockRepo = new Mock<IRepository>();
Mock<UserManager<AppUser>> userMgr = GetMockUserManager();
userMgr.Setup(x => x.FindByNameAsync(It.IsAny<string>())).ReturnsAsync(new AppUser() { UserName = "JohnDoe", Id = "1" });
var contextUser = new ClaimsPrincipal(new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.NameIdentifier, user.Id),
}));
Mock<ITempDataDictionary> tempData = new Mock<ITempDataDictionary>();
ProjectController controller = new ProjectController(mockRepo.Object, userMgr.Object)
{
TempData = tempData.Object,
ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext() { User = contextUser }
}
};
Project project = new Project()
{
Name = "Test",
UserID = "1",
};
//Act
Task<IActionResult> result = controller.EditProject(project);
//Assert
mockRepo.Setup(m => m.SaveProject(It.IsAny<Project>(), user));
//This line still throws an error
mockRepo.Verify(m => m.SaveProject(It.IsAny<Project>(), user));
Assert.IsType<Task<IActionResult>>(result);
var view = result.Result as ViewResult;
Assert.Equal("ProjectCharts", view.ViewName);
Assert.Equal("Project", view.Model.ToString());
}
在調試的時候,我可以驗證該方法確實是在控制器中調用的,
While debugging, I can verify that the method is actually called in the controller,
//This controller line is touched walking through the code
repository.SaveProject(project, user);
//but this repo line is not touched
public void SaveProject(Project project, AppUser user)
調試實際上并不顯示進入存儲庫方法.確切的錯誤如下
Debugging doesn't actually show entrance into the repository method. The exact error is below
預期至少對模擬調用一次,但從未執行過:m => m.SaveProject(, JohnDoe)
Expected invocation on the mock at least once, but was never performed: m => m.SaveProject(, JohnDoe)
未配置任何設置.執行的調用:IRepository.ProjectClassIRepository.SaveProjects(ProjectClass, JohnDoe)'
No setups configured. Performed invocations: IRepository.ProjectClass IRepository.SaveProjects(ProjectClass, JohnDoe)'
當我進行實際的集成測試時,SaveProject
方法在存儲庫中被觸及,并且似乎工作正常.我也嘗試在單元測試中分配每個 Project
屬性,但得到相同的錯誤結果
When I do an actual integration test, the SaveProject
method is touched in the repository and seems to work properly. I've also tried assigning every Project
property within the unit test but got the same error result
推薦答案
我會比 Yoshi 的評論更進一步.
I'm going to go a step further than Yoshi's comment.
Performed invocations
消息告訴您該方法已被調用,但沒有使用您正在驗證的參數.根據消息,我的猜測是第一個參數有問題.
The Performed invocations
message tells you the method was called but not with the parameters that you were verifying. My guess based on the messages is that there's something wrong with the first parameter.
您需要發布測試讓我能夠更具體.
You would need to post the test for me to be able to be more specific.
更新(添加測試后)
更改 userMgr.Setup
以返回您的用戶"變量,而不是重復.盡管我之前說過,這是您失敗的原因 - 正在測試的代碼被重復,并且 Moq 正確地說您的方法沒有被 user
調用,因為它已被調用與副本.所以把它改成這個可以解決問題:
Change userMgr.Setup
to return your 'user' variable, not a duplicate. Despite what I said earlier, this was the cause of your failure - the code being tested was being given a duplicate, and Moq was correctly saying that your method had not been called with user
because it had been called with the duplicate. So changing it to this fixes the problem:
userMgr.Setup(x => x.FindByNameAsync(It.IsAny<string>())).ReturnsAsync(user);
如果可以避免使用 It.IsAny<string>()
,這可能會變得更加強大:如果將預期作為參數的特定字符串設置為測試的一部分設置,然后給出值.
This could be made even stronger if the use of It.IsAny<string>()
can be avoided: if the specific string that is expected as a parameter is set up as part of the test setup, then give the value instead.
我懷疑兩個1"字符串需要相同才能使這項工作正常工作,因此不要復制字符串,而是聲明一個局部變量并使用它而不是兩個字符串.
I suspect both of the "1" strings need to be identical to make this work, so rather than duplicate the string declare a local variable and use that instead of both strings.
我建議永遠不要使用像 1 這樣的值;更喜歡隨機輸入一些東西,以免它偶然通過.我的意思是,想象一個將兩個整數作為參數的方法:當為該方法調用 Setup 或 Verify 時,如果您對這兩個整數使用相同的值,即使您的代碼錯誤地交換了值,測試也可以通過 (將每個傳遞給錯誤的參數).如果在調用 Setup 或 Verify 時使用不同的值,那么只有在正確的參數中傳遞了正確的值時才會起作用.
I would suggest never using values like 1; prefer to randomly type something, so that it doesn't coincidentally pass. By which I mean, imagine a method which takes two integers as parameters: when calling Setup or Verify for that method, if you use the same value for both those integers, the test could pass even if your code has mistakenly swapped the values over (passing each into the wrong parameter). If you use different values when calling Setup or Verify, then it will only work when the correct value is passed in the correct parameter.
mockRepo.Setup
是多余的.安裝程序允許您指定類的行為方式,但在此之后沒有其他內容,因此它是多余的并且可以刪除.有些人將 setup 與 VerifyAll 一起使用,但您可能想閱讀有關使用 VerifyAll.
mockRepo.Setup
is redundant. Setup allows you to specify how the class behaves but there is nothing else after that on the line, so its redundant and can be removed. Some people use setup along with VerifyAll but you might want to read this discussion about using VerifyAll.
現在將您的驗證改回使用 project
而不是 It.IsAny<Project>()
.我希望它能夠工作.
Now change your verify back to using project
rather than It.IsAny<Project>()
. I would expect it to work.
更新 2
考慮一個瓷磚屋頂.每塊瓦片負責保護屋頂的一小部分,與下面的部分略微重疊.使用模擬時,那個瓦屋頂就像一組單元測試.
Consider a tiled roof. Each tile is responsible for protecting one small part of the roof, slightly overlapping the ones below it. That tiled roof is like a collection of unit tests when using mocking.
每個 'tile' 代表一個測試夾具,覆蓋真實代碼中的一個類.重疊"表示類與其使用的事物之間的交互,必須使用模擬定義,模擬使用諸如設置和驗證(在 Moq 中)之類的東西進行測試.
Each 'tile' represents one test fixture, covering one class in the real code. The 'overlapping' represents the interaction between the class and the things it uses, which has to be defined using mocks, which are tested using things like Setup and Verify (in Moq).
如果這個模擬做得不好,那么瓷磚之間的間隙會很大,并且您的屋頂可能會泄漏(即您的代碼可能無法正常工作).模擬如何做得不好的兩個例子:
If this mocking is done badly, then the gaps between the tiles will be big, and your roof could leak (i.e. your code might not work). Two examples of how mocking can be done badly:
- 當你真的不需要時,通過使用
It.IsAny
不檢查提供給依賴項的參數. - 與實際依賴項的行為方式相比,mock 行為的定義不正確.
- Not checking the parameters which are given to the dependencies, by using
It.IsAny
when you really don't need to. - Incorrectly defining the behaviour of the mock compared to how the real dependency would behave.
最后一個是你最大的風險;但這與編寫糟糕的單元測試的風險沒有什么不同(不管它是否涉及模擬).如果我編寫了一個單元測試,它執行了被測代碼,但沒有做出任何斷言,或者對無關緊要的東西做出斷言,那將是一個弱測試.使用 It.IsAny
就像在說我不在乎這個值是什么",這意味著您錯過了斷言該值應該是什么的機會.
That last one is your biggest risk; but it's no different than the risk of writing bad unit tests (regardless of whether it involves mocking). If I wrote a unit test which exercised the code under test but then failed to make any assertions, or made an assertion about something that doesn't matter, that would be a weak test. Using It.IsAny
is like saying "I don't care what this value is", and means you're missing the opportunity to assert what that value should be.
有時無法指定值,而您必須使用 It.IsAny
,我稍后會回到另一種情況也可以.否則,您應該始終嘗試指定參數是什么,或者準確地指定,或者至少使用 It.Is<T>(comparison lambda)
.另一次可以使用 It.IsAny<T>()
是當您使用 Times.Never 驗證調用是否
作為 Verify
的參數.在這種情況下,始終使用它通常是一個好主意,因為它會檢查是否沒有使用任何參數進行調用(避免您只是在給定參數時出錯的可能性).
There are times when it's not possible to specify the value, where you have to use It.IsAny
, and one other case I'll come back to in a second is also OK. Otherwise, you should always try to specify what the parameters are, either exactly, or at least using It.Is<T>(comparison lambda)
. The one other time it's ok to use It.IsAny<T>()
is when you are verifying that a call has not been made, using Times.Never
as a parameter to Verify
. In this case, it is usually a good idea to always use it, since it checks the call has not been made with any parameter (avoiding the possibility that you have simply made an error on what parameters are given).
如果我寫了一些單元測試,它給了我 100% 的代碼覆蓋率;但沒有測試所有可能的場景,那將是弱單元測試.我是否有任何測試來試圖找到這些寫得不好的測試?不,不使用 mocking 的人也沒有這樣的測試.
If I wrote some unit tests which gave me 100% code coverage; but didn't test all the possible scenarios, that would be weak unit testing. Do I have any tests to try to find these badly written tests? No, and people who don't use mocking don't have tests like that either.
回到瓷磚屋頂的類比...如果我沒有模擬,并且必須使用真正的依賴項測試每個部分,這就是我的屋頂的樣子.我可以為屋頂底部邊緣的所有鉆頭鋪上瓷磚.到目前為止沒有問題.對于屋頂上的下一組瓷磚,對于一塊瓷磚,我需要一個三角形瓷磚,覆蓋該瓷磚將去的地方,并覆蓋它下面的瓷磚(即使它們已經被一塊瓷磚).不過,還不錯.但是屋頂再往上 15 塊瓷磚,這會讓人筋疲力盡.
Going back to the tiled roof analogy... if I didn't have mocking, and had to test each part using the real dependencies here's what my roof would look like. I could have a tile for all of the bits at the bottom edge of the roof. No problem so far. For what would have been the next set of tiles up the roof, for what would have been one tile, I need a triangular tile, covering where that tile would have gone, and covering the tiles below it (even though they are already covered by a tile). Still, not too bad. But 15 tiles further up the roof, this is going to get exhausting.
將其帶到現實世界的場景中,假設我正在測試一段客戶端代碼,該代碼使用兩個 WCF 服務,其中一個是按使用收費的第三方,其中一個受 Windows 身份驗證保護,也許其中一個服務在到達數據層并與數據庫交互之前在其業務層中具有復雜的邏輯,并且在那里的某個地方,我可能有一些緩存.我敢說為此編寫體面的測試而不用嘲笑可以被描述為過于復雜,如果它甚至可能的話(在一個人的一生中)......
Bringing that to a real world scenario, imagine I'm testing a client-side piece of code, which uses two WCF services, one of which is a third party that charges per use, one of which is protected by windows authentication, maybe one of those services has complex logic in its business layer before reaching the data layer and interacting with a database, and somewhere in there, I might have some caching. I daresay writing decent tests for this without mocking could be described as overly-convoluted, if it's even possible (in one person's lifetime)...
除非您使用模擬,否則您可以...
Unless you use mocking, which allows you to...
- 在不調用第三方代碼的情況下測試您的代碼(承認前面提到的關于準確模擬的風險).
- 模擬如果有或沒有正確權限的用戶調用受保護的 WCF 服務會發生什么(想想如何在沒有模擬的情況下從自動化測試中做到這一點)
- 單獨測試代碼的各個部分,這在涉及復雜業務邏輯的情況下尤其有用.這成倍地減少了需要測試的代碼路徑數量,降低了編寫測試和維護測試的成本.想象一下,必須設置具有所有先決條件的數據庫的復雜性,不僅針對數據層測試,而且針對調用堆棧上的所有測試.現在,當發生數據庫更改時會發生什么?
- 通過驗證您的模擬方法被調用的次數來測試緩存.
(為了記錄,測試的執行速度對我使用模擬的決定沒有任何影響.)
(For the record, speed of execution of the tests has never played any part in my decision to use mocking.)
幸運的是,嘲弄很簡單,幾乎不需要任何程度的理解就可以超出我在這里所闡述的內容.只要您承認與全面集成測試相比,使用模擬是一種折衷方案,它就可以節省開發和維護時間,任何產品經理都會感激不盡.所以盡量保持瓷磚之間的間隙很小.
Luckily mocking is simple, requiring barely any level of comprehension above what I have spelled out here. As long as you acknowledge that using mocking is a compromise compared to full-on integration testing, it yields the kind of savings in development and maintenance time that any product manager will be grateful for. So try to keep the gaps between your tiles small.
這篇關于模擬驗證()調用的文章就介紹到這了,希望我們推薦的答案對大家有所幫助,也希望大家多多支持html5模板網!