Fakes for Application.FileDialog? #6230
-
Hello @retailcoder & everyone in the team, a thousand thanks for developing Rubberduck - I am really happy about this great add-in! I am not a professional programmer but a mechanical engineer who is using Excel VBA for computation projects. Recently I decided to learn how to use unit tests and TDD, and Rubberduck has helped me a lot to get started. But I am still trying to figure out how to do this correctly, so maybe my question is somewhat stupid. I would like to test the return value of a function that uses the Application.FileDialog property. Since it requires user input, it is not straightforward to be used in an automated test. So far, I did not really understand whether Rubberduck can also fake this kind of user dialog or not. I have successfully used the Fakes.MsgBox in other tests I wrote, but I don't know how to make this function testable without the need of user interaction - or if it is even reasonable to do so. How can I achieve this? Would I need to write my own fake or mock? How would that be done? Many thanks for any help! Best regards, |
Beta Was this translation helpful? Give feedback.
Replies: 7 comments 3 replies
-
Thanks for the feedback! The Rubberduck COM API Fakes module hooks into the VBA7 library and essentially hijacks certain specific internal function calls, like rtcMsgBox and many others. Application.FileDialog lives in the Excel COM library, which Rubberduck is agnostic to, so a different strategy is needed. With mocking you'd write test code that sets up a type that gets materialized at run-time and implements the exact same interface, but then you'd still need a way to provide the mocked dependency to your function under test - and that's where dependency injection comes into play. But Excel.Application is a rather large interface to depend on, which makes it impractical for mocking. So what I'd do here, is to move the dependency to a separate class: write yourself a service class that wraps the FileDialog method, and make your function depend on that class instead. If the function is static / lives in a standard module, it could accept an instance of that service class as a parameter. If the function lives in an object/class instance, then the class instance could accept this service as a "constructor parameter" via a factory method. Either way, test code would now be able to supply an alternative implementation (a test stub class that implements the service class interface). Basically this is a similar situation to, say, popping a UserForm: you decouple the testable logic from the UI by introducing an interface between the two, and then tests inject an alternative implementation that accepts the method call but implements it in a way that doesn't block the execution of a test. Hope it helps! |
Beta Was this translation helpful? Give feedback.
-
Great question, and great answer! I agree with @retailcoder here. Since you say you are not a professional programmer, consider reading about Structural Design Patterns. I'm sure you'll recognize the ones which apply to this situation. P.S. I'm a former mechanical engineer turned professional programmer. Be careful, or you may end up the same as me! ;-) |
Beta Was this translation helpful? Give feedback.
-
I can't believe it but I made it! This is pretty crazy for me because it was my first time working with both interface classes and this idea of service classes and stubs. But without this similar problem explained here in more detail, I would never have been able to figure this out. It took me two days to do it, but now I've learned a lot. Thanks again for your help. |
Beta Was this translation helpful? Give feedback.
-
@owfischer let's see some code... :-) |
Beta Was this translation helpful? Give feedback.
-
Okay! I hope it's not too embarrassing. I am curious about your comments. For the given purpose it feels like a lot of overhead - as @retailcoder commented in the end of the blogpost cited above. And maybe someone could do more efficiently. But for now I am stoked that I have learnt something new! And thanks @pflugs30 for the link on design patterns; I have realized that something like that exists and have started to learn about them. The original function, not testable because depending on user interactionMy standard module LibFileOperations with my function to be tested "SelectFilename" looks like follows. Basically, I just want to pass some parameters to configure the Application.FileDialog window and return the selected path and filename as separate strings. Below, the line
The function to be tested without dependenciesThe refactored function looks as follows:
The service classes to make the function testableThe class MyTestableFileDialog:
The functions SeparatePathAndFileName and EmptyPathAndFileName are not shown here but obviously return what the names say. The abstracted service class IFileDialogService (the interface class):
The concrete service class MyFileDialogService (which calls the real Application.FileDialog in the normal case) looks like that:
My test stub class MyFileDialogServiceStub is this:
Here, in the line The test using the stubThen finally, the test (which calls the FileDialog stub ) looks like this:
Is that somewhat acceptable for my debut work? Greetings, |
Beta Was this translation helpful? Give feedback.
-
At first glance, this code looks like a fantastic step in the right direction. Well done. I think it's worthy of a deeper dive on my part. Please give me a day or two to get back to you. 👍 |
Beta Was this translation helpful? Give feedback.
-
Oliver (@owfischer): Great job with this code, especially for not being a professional programmer! I can tell from your style that you have at least some programming background. It's always fun for me to talk with degreed MEs again. I looked through your code here in Github, and I also put it into a simple Excel file. Sometimes, that just makes it easier for me to review it. All in all, WELL DONE. It looks pretty good, and I won't beat you up about anything. At the end of the day, as one of my former managers said, "to an engineer, a project is always 90% done. But 90% complete published code is better than 100% perfect code in your head!" As such, please consider the following comments as simply another perspective. Try some of them, and see if you like them.
While this works, most testing frameworks would prefer you do something like this: This change is subtle, but it produces more readable code. RubberDuck's test runner can also produce a nice message for you when the test fails.
To improve the readability, I might suggest some name changes:
I could probably add a few more, but I think that's enough for now. Again, really great job. I don't even see code like this from some junior developers for a few months... :) Here's the Excel file I assembled with your code: |
Beta Was this translation helpful? Give feedback.
Thanks for the feedback!
The Rubberduck COM API Fakes module hooks into the VBA7 library and essentially hijacks certain specific internal function calls, like rtcMsgBox and many others.
Application.FileDialog lives in the Excel COM library, which Rubberduck is agnostic to, so a different strategy is needed. With mocking you'd write test code that sets up a type that gets materialized at run-time and implements the exact same interface, but then you'd still need a way to provide the mocked dependency to your function under test - and that's where dependency injection comes into play.
But Excel.Application is a rather large interface to depend on, which makes it impractical for mocking. So…