简体   繁体   中英

Test case for a switch statement C#

I'm a quite new person for C# and unit tests. I need to write a unit tests for a switch statement and I have to admit that I already have been through many of pages trying to find something. Does anyone could give me any tip how to create it? please My code below:

public static Message create(String body)
{
    Message result = null;
    switch (body.ToArray()[0])
    {
        case 'S':
            //checking the regex for a sms message
            Match matchS = Regex.Match(body, @"[S][0-9]{9}\+[0-9]{12}");
            if (matchS.Success)
            {
                MessageBox.Show("This is SMS message");
                //if the regex will match a sms_message class will be started
                result = new Sms_Message(body.Substring(1));
            }
            //if the regex doesn't match the message should be displayed
            else throw new Exception("I don't like content!!!!!!!!!!");       
            break;
        case 'T':
            //checking the regex for a tweet message
            Match matchT = Regex.Match(body, @"[T][0-9]{9}");                     
            if (matchT.Success)
            {
                MessageBox.Show("This is the Tweet message");
                //if the regex match the message should be displayed and the class Tweet will be started         
                result = new Tweet(body.Substring(1));
            }
            break;
        case 'E':
            //checking the regex for a email message
            Match matchE = Regex.Match(body, @"[E][0-9]{9}"); 
            if (matchE.Success)
            {
                //checking the content of the message by using function 'BodyIsSir'
                if (BodyIsSir(body))
                    //if function return true the SIREmail class will be started                                               
                    result = new SIREmail(body.Substring(1));                       
                else
                    //if function return false the StandardEmail class will be started
                    result = new StandardEmail(body.Substring(1));                  
            }
            //when regex will not match the text message will be displayed
            else throw new Exception("I don't like the content");                   
            break;
        default:
            //when the content of the email message will not match
            throw new Exception("I dont like first letter!");                         
    }
    return result;
}

private static bool BodyIsSir(string body)
{
    //checking the body of email message if this contain regex for checking the sort code
    Match matchSIR = Regex.Match(body, @"[0-9]{2}[-][0-9]{2}[-][0-9]");              
    if (matchSIR.Success)
        return true;                                                                
    else
        return false;                                                                   
}

I need to write a unit tests for a switch statement

No you don't, because that's not a unit.

The unit is:

  1. public static Message create(String body) (That is to say, what you can see of the code, from the outside).
  2. Any state that affects the behaviour of that method so that it might do something different for the same body . (One of the advantages of methods that aren't affected by state but only by inputs is that it therefore makes testing easier).
  3. The documented behaviour of what it should do, whether that documentation is written down or just in your head. (It being just in your head is a bad thing, but in some cases tests themselves can serve as low-level documentation of what is expected behaviour).

You write your unit tests to:

  1. Perhaps arrive at the correct point to begin with (if you write the test before the code, which is often a good approach).
  2. Assure yourself, and others, that the code works correctly.
  3. Catch bugs caused by later changes accidentally breaking this piece of code. (In particular when it comes to your question. If the switch was replaced by some other mechanism, the tests should not change, but you aren't testing a switch any more).

So, you want to write tests that do this. The basic approach is simple enough, you have a bunch of different inputs, a bunch of different expected outputs or exceptions, and you write a test to check on it.

You don't say what testing framework you are using. I recommend XUnit , but NUnit and MSUnit are also good.

Keep unit tests small, testing just one thing*, though perhaps examining a few features of those things†. For example:

[Fact]
public void SMSMessage()
{
  Message msg = YourClass.create("S123456789+123456789012");
  Assert.IsType<Sms_Message>(msg);
  Assert.Equal("123456789+123456789012", msg.Body);
}

(In NUnit or MSUnit [Fact] would be [Test] , .IsType would be .IsInstanceOfType and .Equal would be .AreEqual )

Checking exceptions is just as important as checking correct uses. One of the reasons for preferring XUnit over NUnit or MSUnit is that while XUnit and MSUnit have an [ExpectedException] attribute that defines the type of exception expected, XUnit has a method that allows for better checking the precise call that throws the exception (so tests can't indicate success by throwing the right exception at the wrong time) and allows for examining the exception thrown:

[Fact]
public void InvalidSMS()
{
  Exception ex = Assert.Throws<Exception>(() => YourClass.create("S12345"));
  Assert.Equal("I don't like content!!!!!!!!!!", ex.Description);
}

You can also carry out tests over a large range of data:

public static IEnumerable<object[]> ValidSMSMessages()
{
  yield return new object[] { "123456789-123456789012" }
  yield return new object[] { "123456912-123456789012" }
  yield return new object[] { "123672389-123456789012" }
  yield return new object[] { "121233789-123456789012" }
  yield return new object[] { "123456789-123456781212" }
  yield return new object[] { "123456789-121216789012" }
  // One could probably think of better examples here based on a mixture of realistic and edge-case uses.
}

[Theory]
[MemberData("ValidSMSMessages")]
public void SMSMessages(string smsBody)
{
  Message msg = YourClass.create("S" + smsBody);
  Assert.IsType<Sms_Message>(msg);
  Assert.Equal(smsBody, msg.Body);
}

Always try to think of the edge-cases. For example, if null or an empty string can be passed to a method then you should test doing so, and that one either gets the correct result (if doing so is valid) or the correct exception (if doing so is invalid). (It's a bonus here that Assert.Throws<> when the type is either ArgumentException or derived from it has a form that takes an argument with the expected parameter name)

[Fact]
public void NullBody()
{
  Assert.Throws<ArgumentNullException>("body", () => YourClass.create(null));
}

[Fact]
public void EmptyBody()
{
  Assert.Throws<ArgumentException>("body", () => YourClass.create(""));
}

Note that the code in your answer fails both these tests. Hurray! Our testing has found two bugs to fix.

(It's not clear to me whether the fact that input of "T" would return null is a bug or by design. This is one reason why I prefer to return out of switch blocks immediately rather than assign within them and then return at the end; if this approach was taken you would have had to return null explicitly if you wanted it, or a compiler error. So it would either be obvious to someone reading the code that returning null was correct, or if incorrect it would have been fixed).

What we can't find as easily with unit tests are design flaws. In the code in question in there are the following design flaws:

  1. Uses names which don't follow .NET conventions (lower-case method name, use of underscore in method names).
  2. Throws Exception rather than a more derived type more specific to the case.
  3. Doesn't separate business and display logic, but calls MessageBox.Show() from within a factory method.
  4. Calls ToArray() which wastes time and memory allocating a char[] just to access [0] on that array, when replacing body.ToArray()[0] with body[0] would have the same effect more efficiently.

However:

  1. Thinking about how to test a method forces you to think about how that method should work, which can make design flaws you hadn't noticed more apparent.
  2. Unit tests make improvement safer. Say we only realised the waste of the ToArray() call some time afterwards. With unit testing in place we can run the unit tests again after taking it out. If our performance improvement somehow broke something we'd know (it won't, but if we could be right about everything like that all the time we wouldn't need any testing at all…). Conversely, while the tests all running doesn't prove we haven't broken anything, they can certainly increase our confidence that we haven't.

Use coverage tools as a guide, not a crutch. Don't look at coverage reports when you first write your tests. Then when you do, find the code paths that your tests aren't covering, think about what sort of situations would involve them, and then add tests for those cases and similar cases without looking at the coverage while you improve it. This way coverage really does guide you to write better tests, but if you are constantly looking at the coverage it can be easy to fall into the trap of writing tests that get "perfect" coverage without actually testing much. Tests with poor coverage that cover a wide variety of cases are better than tests with 100% line and branch coverage that don't really exercise the possible permutations. (Of course, its also possible that a branch isn't covered because it's impossible and then you can remove the dead code and/or replace a branch with an assertion that the branch is unreachable).


*It's possible that somebody reading this may have seen unit tests written by me on open-source projects that broke this rule. I got better.

†An example of testing lots of features that are valid is that if perhaps a method was supposed to return an IList<T> that was read-only, it's reasonable to test all of the features of a read-only IList<T> in the same test, as they're all aspects of the same well-defined concept. Eg:

[Fact]
public void ResultIsReadonly()
{
   IList<int> list = SomeMethodThatReturnsAReadonlyList();
   Assert.True(list.IsReadonly);
   Assert.Throws<NotSupportedException>(() => list.Add(5));
   Assert.Throws<NotSupportedException>(() => list.Clear());
   Assert.Throws<NotSupportedException>(() => list.Insert(0, 1));
   Assert.Throws<NotSupportedException>(() => list.Remove(1));
   Assert.Throws<NotSupportedException>(() => list.RemoveAt(0));
   Assert.Throws<NotSupportedException>(() => list[0] = 1);
}

While this has seven assertions, they are all necessary to test the same feature of the result. Conversely, if we were more concerned about a class that implemented a read-only list than a method that returns one, we should probably be thinking about those features severally.

In my point of view, you should create several test for every case you have. You can make helper interface and class (I'll call it IController ) with different methods to different case.

     IController controller;
     case 'S':
         controller.CaseS ();
        break;
      case 'T':
          controller.CaseT(body);
          break;
      case'E':
            controller.CaseE(body);
          break;
      default:
          throw new Exception("I dont like first letter!");   

And make small unit tests like: if body starts with 'S' it will call controller.CaseS () and so on, using your IController mock object. And then make tests to cover behaviour of not mock implementation of IController .

Or make several tests with different inputs: if body equal "S..." if should return this result. If body equal 'P..' it throws exception

Also, I suggest to avoid using MessageBox.Show dirreclty and create IMEssageBoxHelper with Show method and use it instead of MessageBox.Show() , so in your tests you can change it to mock implementation, so your tests will run without messageboxes.

If you would like to test the possible cases, you can use TestCaseSource attribute on test method. This will iterate on the specified source and call the test method with each item.

[TestFixture]
public class MessageTests
{
    static Case[] ValidCases = {
        new Case ("S...", typeof(Sms_Message)),
        new Case ("T...", typeof(Tweet)),
        new Case ("E...", typeof(SIREmail)),
        new Case ("E...", typeof(StandardEmail))
    };

    [Test]
    [TestCaseSource("ValidCases")]
    public void Create_ShouldCreate_WhenValidSource(Case currentCase)
    {
        // when
        Message created = TargetClass.create (currentCase.Body);

        // then
        Assert.That (created.GetType (), Is.InstanceOf (currentCase.ExpectedResultType));
    }

    public class Case
    {
        public Case(string body, Type expectedResultType)
        {
            Body = body;
            ExpectedResultType = expectedResultType;
        }

        public string Body { get; private set; }
        public Type ExpectedResultType { get; private set; }
    }
}

When testing any method you need to consider the behaviour that any client will experience when invoking your method. Essentially this boils down to asking the simple question:

  • For a given input, what is the expected output/result for this method?

By looking at the different "branches" in your method, you can work out the different inputs you need to test to gain "full coverage" of your method. For example, in your method there are 2 possible outcomes for inputs starting with S : either it matches the regex - or it throws an exception. Similar analysis should yield a set of inputs that should cover every possible path through your method.

It is imperative that you remove the dependency on MessageBox.Show() on your method - as any automated test is going to halt at this step and require manual intervention for you to actually get any result out of the method. The suggestion in the answer here is a good one - to remove your dependency on this using an interface.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM