2

I am new to unit testing and fairly new to c# and would really appreciate some help with how to write a unit test to ensure the logic in the AddGrade method is working. So basically if the grade is >=0 and <=100 then the grade is valid and it will be added, anything else isn't and it should display an error in the console.

I have seen another post about unit testing an if-else statement in c# and tried working this out from that but It confused me if I am honest. I have looked around online etc and tried a lot of different things to try and figure this out but I find it hard sometimes to apply peoples example to my code so I thought it best just to post my code and get some help, any help would be greatly appreciated :)

I am using Xunit for the unit tests. This project runs in the console.

This is the program class with the main method

    using System;
using System.Collections.Generic;

namespace GradeBook
{
    class Program
    {
        static void Main(string[] args)
        {
            var book = new Book("Dave's");

            //add grade to the book
            book.AddGrade(90.5);
            book.AddGrade(80.5);
            book.AddGrade(70.5);
            book.AddGrade(60.5);

            var stats = book.GetStatistics();
            Console.WriteLine($"This Gradebooks name is {book.Name}");
            Console.WriteLine($"The highest grade is {stats.High:N1}");
            Console.WriteLine($"The lowest grade is {stats.Low:N1}");
            Console.WriteLine($"The average grade is {stats.Average:N1}");//N1,N2 etc. is number of decimal places
        }
    }
}

This is the book class where the AddGrade method is I want to write the unit test for

    using System;
using System.Collections.Generic;

namespace GradeBook
{
    public class Book
    {
        public Book(string name)
        {
            grades = new List<double>();
            Name = name;
        }

        public void AddGrade(double grade)
        {
            if (grade >= 0 && grade <= 100)
            {
                grades.Add(grade);               
            }
            else
            {
                Console.WriteLine("Please enter a value between 0 and 100");
            }
        }

        public Statistics GetStatistics()
        {
            var result = new Statistics();
            result.Average = 0.0;
            result.High = double.MinValue;
            result.Low = double.MaxValue;
            

            foreach (var grade in grades)
            {
                Console.WriteLine($"{Name} grade is: {grade}");
                result.Low = Math.Min(grade, result.Low);
                result.High = Math.Max(grade, result.High);
                result.Average += grade;
                
                
            }
            result.Average /= grades.Count;
            return result;           
        }

        private List<double> grades = new List<double>() { };
        public string Name;
    }
}

This is the Statistics class

    using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace GradeBook
{
    public class Statistics
    {
        public double Average;
        public double High;
        public double Low;
    }
}

This is where I am writing the unit test for this

    using System;
    using System.Collections.Generic;
    using Xunit;
    
    namespace GradeBook.Tests
    {
        public class BookTests
        {
            [Fact]
            public void BookGradeIsValid()
            {
                var book = new Book("");           
                book.AddGrade(105);
            }
        }
    }
SomeBody
  • 4,044
  • 1
  • 10
  • 26
SkyFire
  • 39
  • 3
  • Welcome to the wonderful world of unit testing! Where is the list "grades" defined? Is it publically accessible in the scope of BookTests? – Rasmus Dalhoff-Jensen Mar 09 '21 at 10:48
  • 2
    When your code is hard to test it is most likely because it is not written to be easily testable. Here i think your hard dependency on the Console is a problem. You can't easily mock console away. So either try to get rid of console in the code complete by returning a result that indicates success or not. Throw an exception instead or encapsulate Console with a different class that has an interface that can be mocked and cleanly tested against. – Ralf Mar 09 '21 at 10:49

1 Answers1

6

I recommend that you modify your AddGrade() method to throw an exception if an invalid grade is added:

public void AddGrade(double grade)
        {
            if (grade >= 0 && grade <= 100)
            {
                grades.Add(grade);               
            }
            else
            {
                throw new ArgumentException("grade must be between 0 and 100");
            }
        }

You can test it like this:

[Fact]
public void BookGradeIsValid()
{
      var book = new Book("");           
      Assert.Throws<ArgumentExeception>(() => book.AddGrade(101)); // should throw
      Assert.Throws<ArgumentExeception>(() => book.AddGrade(-1)); // should throw
      book.AddGrade(0); // should not throw
      book.AddGrade(100); // should not throw
}

As a hint, don't use 105 for testing. Use a value directly at the boundary. If you had a typo and you wrote if (grade >= 0 && grade <= 103) instead of 100, you would not realize it with your test.

Using the exception has several advantages. First, you can use the Book class also in a non-console app. Second, you can do a custom error handling if something went wrong:

int grade = 300;
try
{
book.AddGrade(grade)
}
catch(ArgumentException)
{
   // so many possibilities: log to console or display a MessageBox or
   // send an e-mail to system admin or add grade with a default value or ...
}

Also, you can't ignore the exception. If you add a grade with an invalid value, your program will stop there, and you don't wonder 200 lines later in the code, why one grade is missing.

SomeBody
  • 4,044
  • 1
  • 10
  • 26
  • 2
    May I suggest to use [ArgumentOutOfRangeException](https://docs.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception)? Another suggestion is to split test in multiple with [proper names](https://stackoverflow.com/a/1594049/1997232) or at least using set of parameters (`InlineDataAttribute`?) as single test is supposed to perform single *acting*. – Sinatr Mar 09 '21 at 11:19
  • Thanks for the suggestion, it has really helped and I understand why this is works as well, and why using a boundary value is a good idea too. – SkyFire Mar 09 '21 at 11:33