Is THIS The Best Way To Parameterize Tests? - xUnit Theory Data
August 12, 2024
• 710 views
testingtesttestssoftware testingC# testingxunitquality assurancexunit parameterized testsnet corexunit attributesxunit factxunit theoryxunit vs nunitacceptance testsunit testingmstest vs nunit vs xunitxunit vs mstestdotnettheory datatest driven developmentsoftware testing pyramidcode coverageautomated testdotnet xunittests in C#how to use xunitxunit tutorialxunit tutorial c# for beginnersxunit tutorial c#tddc#csharp
xUnit is a popular testing framework that we can use to test our dotnet applications.
But it's got a smell that I've been putting up with for years.
The parameterized test data sucks. We lose all of our type safety when creating generators for object arrays! Yuck!
But what if there was a better way? What if we could ensure type safety with parameterized tests in xUnit?
Let's check out Theory Data!
View Transcript
xunit is a powerful unit testing framework that we have available to us in net and personally it's one of my favorites but xunit has always had something a little bit weird about it that I haven't really liked and I wish I knew about this feature sooner hi my name is Nick centino and I'm a principal software engineering manager at Microsoft when it comes to writing tests and xunit and in particular when we're working with theories which are parameterized tests and xunit the types that we pass into the test versus where we're generating the test data from they don't have to match up it's kind of weird we have to deal with the object type the base type in C so in this video we're going to look at Theory data as a solution to that so if that sounds interesting remember to subscribe to the
channel and check out that pin comment for my courses on dome train that said we'll jump over to visual studio we'll look at a very simple example and walk through Theory data on my screen I have a very simple parameterized test here you can see that it's add valid inputs and then it's looking for the expected value and really it's very simple right we're going to create an Adder as you might expect if I scroll down a little bit lower the adder is just going to add two numbers together the system under test is not really important here we're going to be looking at the data that we're passing around and how to leverage xunit so if we look back up at the theory so in particular from line 9 to 13 you can see that I am marking the test method as Theory and
then I have the parameters passed into it so just the first number the second number and then the expected result that when we add these two things together that's what we're testing for you'll also notice that I've marked this one up with member data and then I have to give it the name of the member of this class that has the test data on it so this is the traditional way that we usually do this this is what we've had available to us in the past when I started using xunit this is really what I started using and I've kind of just stuck to it because that's the way we've always done it right so if you look closely what's kind of gross about using this is that this is going to be a generator it's going to yield back the sets of data that
we're working with and you'll notice that the type is an object array so that means that I'm going to create an object array here there's literally nothing stopping me from putting a string in here this compiles if we were to go run this this would totally break now because when it goes to pass the around at runtime it can't put this string into this first parameter so we lose all of the type safety that's kind of crappy because we're working with a language that is really good at this right this is what I've been used to doing and if we go run these right now because all of the types are right if I go press play run them all not super exciting but we're going to get a green light on all of them right so this does work uh the math you can
trust me on it right you just saw the test pass it's not really the important part but I just wanted to have a set of data that we can work with this is one way that you can do this with the member data you can also use class data and really Define a whole whole separate class that has the test data in it that's actually very handy for reusability for different scenarios to work through a lot of the time in my personal projects I find that member data just works conveniently then I have the test data right around the test that I'm running lots of different ways to do it but this is the traditional way that we've had to work with so we're going to look at the first variation of this and in fact we're going to jump over to using class data
so I'm going to swap this out and I'm going to put class data on here but we don't have the calculator test data yet so let's go look at that this is just a brief Interruption to remind you that I do have courses available on dome train focused on C so whether you're interested in getting started in C looking for a little bit more of an intermediate course focus on object-oriented programming and some async programming or you're just looking to update your refactoring skills and see some examples that we can walk through together you can go ahead and check them out by visiting the links in the description and the comment below thanks and back to the video okay so we're going to look at the first example of theory data which is going to be the base class for our calculator test data you'll
notice this Theory data takes in these parameters right these tight parameters at the end and if you think about what we were doing back up at this set of data up here right if I do a little bit of block select and we look at these these are just three integers and that's exactly what I have down here on line 76 so this Theory data is going to take three integers one for the first number to add one for the second number to add and the third parameter is the result and if we have a close look inside when we go to create this calculator test data inside the Constructor we just call this built-in ad method not to be confused with the system under test ad method this is going to add this information into the theory data sorry for the confusing naming in
this example but this is adding it to the theory data so we're just building up that collection now I've just put in the same set of test data here right so what I've highlighted here from line 80 to 84 is the same as from line 2 2 to 26 same data now what I've done if I go back up here is that I've just made this the class data right so it's not member data it's class data now and that way when we're defining this stuff it has to adhere to the same types that we have right here I'm going to go run this one it'll pass um hopefully unless I've totally butchered something but we'll see that we get a green light and that means that these test pass so we are using Theory data but there are some things that we might want
to consider especially when we're comparing to this basee case while this is helpful for having us make sure that this part has Type safety this doesn't give us full type safety so I'm going to explain in just a moment here one of the problems that we still run into right so we have three integers up here I've made this three integers but what would happen if we said instead we're going to make this a string right so just to quickly comment this part out and if I go ahead and let co-pilot come up with something come on co-pilot not being helpful okay so if we put a string and then two other numbers here this compiles right we're meeting this requirement here because a string and then two numbers just like we see this part up here is not doing the check so in fact
I can go run this it does compile and when I run it we'll see that we get one failing and it's going to say that a string cannot be converted to an integer so we do get the protection that when we're defining these we get the right type matching but when we go to pass this into the Tes still it's not going to match we're going to come back to this later in the video I just wanted to show you that we're only partway there but in my opinion this is still a nice little Improvement because that means that we can't mess it up when we're declaring the test data at a minimum so small Improvement but we can do better still so let's go ahead and move on to member data okay now I'm going to comment out the original member data that we
had in here I put this member data attribute back onto the test method and I've added in this Adder test data but this one is a little bit different if we compare what we have it's still going to be public static it's still going to have the same name but you can call it whatever you want but you'll notice that this one was a method and this one is a getter property so on this getter property you'll also notice the return type is Theory data just like we saw with the class data below right in that example we had three integers we have the same thing here and I'm just declaring Theory data and then I'm populating that theory data with our different scenarios that we want to work with it's almost exactly like the class data in terms of how it's set up we
make a new instance of theory data we populate it and then we have it basically called from up here that's how we can use the name of um call here and get the same name of the property so when we go run this xunit knows to go look for this property and this Theory data can get passed in if I go run this so you might expect it will pass hopefully the numbers and the math all check out so we get five tests passing there's our green light now that we've seen the green light for the member data and we had the thing that we had to go explore with the class data right when we Chang the types let's go see what happens when we do the same thing here right so we do get the protection inside here that I can't just go
ahead and make this one a string right we get the type safety within here that's great but what happens when we go do the same thing that we did with the class Theory data so if I go make this a string and then I go make this a string this all compiles but wait a second this actually doesn't compile now so this is giving us more type safety if we hover over this it says the type argument string from Adder test data is not compatible it knows it's able to infer that this type parameter was changed and it's not compatible in the first example we looked at with the theory data it didn't matter we ended up getting to run it it compiled totally fine and when we ran it it failed so so far it looks like the member Theory data does have an
advantage over the class Theory data so I just wanted to call that out that that is a difference between these two even though they look very similar and it might have just seemed like the difference was putting it into a class versus putting it into a property but there is some slight difference in the behavior and we do get better type safety so I think that's pretty cool it kind of works in my favor because I do like using the member data over other dedicated class data but there's one more evolution of this that I want to look at that I prefer even more all right this is the final evolution of the theory data that we're going to look at so if we compare these together it's just a little bit different and you'll notice that instead of having three tight parameters if you
go back to the the working case it was three integers right we're adding three or we're adding two numbers together and having the return value but this one you'll see it's just one type parameter so what I've done is I've made a record down here so this is just a class and it has three inputs on it so the two numbers that we're adding together and then the result what I like to do is I like calling these test scenarios so this is going to be the adder test scenario you can call them whatever you want that's the naming convention that I like to use and then we go make the theory data the same way when we create the theory data we're creating this collection here and then I'm just making individual little scenarios here the nice thing about using a record here is
that we do get that two string characteristic of Records which is really great when we go to look in the test Explorer and we can see the parameters being passed in because it will go two string your inputs so we get that that's really nice but what's really cool about this is if we go back up here if you have tests where you have a lot of parameters being passed in for whatever reason we can collapse that back down to one now so we don't have three things that get passed in we have Adder test scenario and we could call it scenario and then what I would do is scenario. number one number two and then we can get the result off of the scenario so I personally like doing this because I don't have to worry about a bunch of different parameters being passed
in it's just one I like personally just having records and having the two- string ability in the test Explorer yes if just using primitive types in the previous example we saw those will of course two string as well but there's something in my opinion about just having these organized in a nice way if you have more complicated scenarios you could be using uh this type of syntax where you name the parameters and when you're looking through your test data it could make things just a lot more clear so personally I like having the records to work with in my opinion a slight advantage or slight improvement over the previous example but you might find Hey look it's overkill for me in this example right it's just adding a couple of numbers I don't want to go through the overhead of having a whole dedicated type
for it but in my opinion it's a little bit more clean I don't like using the word but uh this just my personal preference for this kind of thing and if we go run this we will see that we get a green light again so there we go all of our test pass and that's going to be Theory data in xunit so to recap on what we saw the original Theory data is not really type safe because it's going to be using an object array coming out of an iterator kind of sucks but it works right this is what I've been personally using for years and years what we saw next was using class Theory data so if I go all the way down here we made this dedicated class it inherits from Theory data with the parameters that we want we do want integers
in this case right from there we ended up seeing that we could go over to member Theory data with a very similar structure this one had a slight advantage that we do get more type safety than the class Theory data and then the final example we saw was using a record like this just to minimize the number of parameters and in my opinion organize your scenarios a little bit more elegantly inside of your test data so I hope you found that helpful and it will get you to structure your tests in a way that's a little bit more readable and a little bit more maintainable thanks so much for watching and I'll see you next time
Frequently Asked Questions
What is xUnit and why is it favored for unit testing?
xUnit is a powerful unit testing framework available in .NET, and personally, it's one of my favorites because it provides a flexible and robust way to write tests.
What are the differences between MemberData and ClassData in xUnit?
MemberData allows you to define test data directly within the test class, which provides convenience, while ClassData requires a separate class for test data, offering better reusability. However, MemberData gives you more type safety compared to ClassData.
How can using records improve the organization of test data in xUnit?
These FAQs were generated by AI from the video transcript.Using records allows me to encapsulate multiple parameters into a single object, making the test scenarios cleaner and easier to manage. It also provides a better string representation in the test explorer, enhancing readability.
