Unit tests don't care about private classes, because unit tests inherently don't care about the processing step. That is to say, unit tests don't care about parts of the processing.
A unit test is inherently only interested in the input and output. As you said, there's only one method with takes a string input and outputs a class. This means that you can't make a unit test smaller than the conversion from the string to the class.
As a somewhat silly example to prove the boundaries of what a unit test is and isn't interested in:
If you put raw chicken in a working oven, then you get cooked chicken. Otherwise, the oven is defective.
What the unit test cares about:
- When I put raw chicken in the oven, cooked chicken comes out of it (test success)
- When I put raw chicken in the oven, something that is not cooked chicken comes out of it (test failure)
- When I put raw chicken in the oven, the oven explodes (fatal test failure)
What the unit test doesn't care about:
- What happens when you put nothing in the oven.
- What happens when you put raw beef in the oven.
- What happens when you put a live chicken in the oven.
- How the
Oven
turns raw chicken into cooked chicken.
- The oven might heat the chicken meat.
- The oven might grill the chicken meat.
- The oven might teleport the raw chicken to the nearest restaurant, have it cooked, and then teleport the cooked chicken back into the oven.
- The oven might simply disintegrate the raw chicken and create cooked chicken out of thin air.
All of these ovens would pass the unit test I mentioned. Because the unit test only cares that the oven yields cooked chicken. It does not care how the oven came by the cooked chicken.
Notice how the unit test doesn't care about what happens when you put other foods in the oven. The test description is only interest in the output when the specified input is given.
Also these tests seem like integration-, rather than unit-tests to me, simply because they test too many lines of code at once - I'm not sure about this though.
The distinction between unit tests and integration tests has nothing to do with how many lines of code are being tested.
Unit tests test the logic in the smallest chunks possible.
Integration tests test whether multiple components are working together.
- If you were testing the inverse method (class to string), that would be a unit test.
- If you were testing the inverse method, including if the file was written to disk (class to file with the string as content), that would be an integration test.
However, I struggle a bit with the thought of re-creating the whole string input with just a slight change for every if in those private methods.
Nothing is preventing you from creating a reusable testing approach. Grossly oversimplified for the sake of example, let's say you have three sections (A, B and C), each section can be one of two options (1 and 2).
private string BuildString(int sectionA, int sectionB, int sectionC)
{
return $"{sectionA}{sectionB}{sectionC}";
}
This can simplify your testing code:
[TestMethod]
public void Test_CanParse_111()
{
var inputstring = BuildString(1,1,1);
var expectedOutput = new MyClass() { A = 1, B = 1, C = 1 };
var receivedOutput = myParser.Parse(inputstring);
Assert.AreEqual(expectedOutput.A, receivedOutput.A);
Assert.AreEqual(expectedOutput.B, receivedOutput.B);
Assert.AreEqual(expectedOutput.C, receivedOutput.C);
}
This simplifies the testing logic as much as it can, while still making sure that the test itself has complete control over the input that is used.
Keep in mind that the unit test shouldn't care how the input string is created. It only cares that the input string always accurately reflects the case that it's testing for.
Looking at the above example, it seems reasonable that A, B and C are separate pieces of logic. One does not influence the other.
If that is the case, then you can write your tests to reflect that.
- A test to see if A = 1 is handled correctly.
- A test to see if A = 2 is handled correctly.
- A test to see if B = 1 is handled correctly.
- A test to see if B = 2 is handled correctly.
- A test to see if C = 1 is handled correctly.
- A test to see if C = 2 is handled correctly.
You don't need to test every combination, you just need to test every individual outcome. If the value of one does not affect the other, there's no point in testing both of them.
For my example, this lowers the amount of tests needed from 8 to 6. That's not a significant change. However, I assume that your input string has more than three properties, each of which can contain more than two possible values. The reduction in amount of tests will significantly increase if there are more properties and possible values.
However, I can see some people on disagreeing on this point, instead suggesting to still test every combination. I disagree with that (I consider it testing beyond reason), but in order to sidestep that discussion altogether, let me offer a second way of simplifying the testing logic:
AutoFixture can be used to randomize your input (with the option of defining reasonable boundaries.
This forgoes a rigid "testing every case" approach, in favor of a "test sampling" approach. Every time the test is run, you get a random value (unless you've specified a constant value you want to be used).
The benefit here is that you don't have to manually write out every combination. It also allows you to test for values that you wouldn't have thought to test for.
The drawback is that you no longer have executive control over the input values. Instead of knowing which value is being used to test, you instead know that e.g. a number between 0 and 9 was used. This obviously also means that you need to be able to discern this value in the output. If that int value causes multiple fields to all be influenced in varied ways, then you should not be testing with reasonably randomized values.
Note that AutoFixture can go hand in hand with the above method. Consider the first test:
- A test to see if A = 1 is handled correctly.
This could be handles by having AutoFixture create your input string with A = 1
(hardcoded), but having B
and C
randomly generated by AutoFixture.
I also see some options which could be of help with that. Lets go with an example of a parser that needs an XML-formatted input string. I could use a hard-coded XML string or I could dynamically create that XML with language-given XML tools (e.g. XDocument in C#).
As per my example above, both are valid approach. Like I said, the unit test shouldn't care how the string was created. It only needs to measure its expectation of the output based on the input string.
Keep in mind here that you're not testing whether your code can generate correct XML. You're testing that your parser can parse XML (that is guaranteed to be structurally correct.
If you make a mistake in the XML generation logic, that should still be visible in the test result (parsing errors); but that doesn't mean that XML validation is the actual intention of the test.
If your XML generation is less than trivial, it wouldn't be the worst idea to first unit test the XML generation itself, before allowing the XML generator to be used in the subsequent parser test. But this gets into a multi-tiered testing approach which is well beyond the initial scope of the question.
If we further think about a program that saves some XML-data in a file to read it later, that program has the corresponding "encoder" for that parser. I could create the XML with the encoder and decode it with the parser/decoder, effectively testing that they work together - which seems like all the program needs anyway, unless the spec says otherwise.
First of all, if you get the file from disk, it's an integration test, not a unit test.
Secondly, I may be missing the point here, but what is the benefit you're hoping to get from using a file from disk instead of an in-memory string? Whether you create that string in a file, or in your IDE, seems to be the same amount of work for the same amount of testing validation.