5
\$\begingroup\$

I have recently taken a small technical test in C# and the following were expected:

  • All stories to be completed with an appropriate level of testing.
  • No actual database implementation is required, feel free to stub it out.
  • Your code should trend towards being SOLID.
  • Make sure you write mock objects and do good unit testing with this.
  • Make sure that your tests actually pass
  • Do not over engineer the test and make sure there is no repetition of code.

Please find my code in GitHub Gift Aid Calculator. The story for the test can be found in the read me section of GitHub.

Program.cs

using System;
using GiftAidCalculator.TestConsole.BusinessObject;
using GiftAidCalculator.TestConsole.Helper;
using GiftAidCalculator.TestConsole.Interface;
using GiftAidCalculator.TestConsole.Repositry;

namespace GiftAidCalculator.TestConsole
{
class Program
{
    static void Main()
    {
        IConfigRepositry config = new ConfigRepositry();

        Console.WriteLine("Please enter the tax rate:");

        decimal taxRate;
        if (ValidateHelper.ValidateDecimal(Console.ReadLine(), out taxRate))
        {
            config.TaxRate = taxRate;
            Console.WriteLine("**************   Promotion     ******************");
            Console.WriteLine("*****    5% Supplement added on running     *****");
            Console.WriteLine("*****    3% Supplement added on swimming    *****");
            Console.WriteLine("**************   Promotion     ******************\n");
            Console.WriteLine("\n Input 1 for Running, 2 for Swimming and 3 for other events.");

            int eventInput;
            if (ValidateHelper.ValidateInteger(Console.ReadLine(), out eventInput))
            {

                Console.WriteLine("\n Please Enter donation amount:");

                decimal donationAmount;
                if (ValidateHelper.ValidateDecimal(Console.ReadLine(), out donationAmount))
                {
                    var calculateTax = new TaxCalculator(config);


                    decimal giftAid = MathHelper.RoundDecimal(calculateTax.CalculateGiftAid(donationAmount));
                    Console.WriteLine("\n Gift aid calculated at the rate of "+ taxRate+" is:" + giftAid + "\n");

                    Console.WriteLine(EventRepositry.GetEventTypeMessage(eventInput)+ "\n");

                    Console.WriteLine("\n The calculated gift amount is: " + (donationAmount +giftAid + MathHelper.RoundDecimal(EventRepositry.GetEventSupplementAmount(eventInput, donationAmount))) + "\n");


                    Console.WriteLine("\n Press any key to exit.");
                    Console.ReadLine();
                }
                else
                {
                    Console.WriteLine("Invalid Input");
                    Console.ReadLine();
                }
            }
            else
            {
                Console.WriteLine("Invalid Input");
                Console.ReadLine();
            }
        }
        else
        {
            Console.WriteLine("Invalid Input");
            Console.ReadLine();
        }

    }

}}

Interfaces

 namespace GiftAidCalculator.TestConsole.Interface
{
  public interface IConfigRepositry
 {
   decimal TaxRate { get; set; }

}
}

namespace GiftAidCalculator.TestConsole.Interface
{
public interface ITaxCalculator
{
    decimal CalculateGiftAid(decimal donationAmount);
}
}

Repositry

using GiftAidCalculator.TestConsole.Interface;

namespace GiftAidCalculator.TestConsole.Repositry
{
  public class ConfigRepositry : IConfigRepositry
 {
    public decimal TaxRate { get; set; }
 }
}


using GiftAidCalculator.TestConsole.Entities;

namespace GiftAidCalculator.TestConsole.Repositry
{
public class EventRepositry
{

   /// <summary>
   /// Gets the event type supplement message.
   /// </summary>
   /// <param name="eventCode"></param>
   /// <returns></returns>
   public static string GetEventTypeMessage(int eventCode)
    {
        string result = string.Empty;

        //validation for invalid event code.
        if (eventCode > 3 || eventCode <1){ eventCode = 3;}

        switch (eventCode)
        {
            case (int)GenericEntities.EventType.Running:
                result = "\n 5% supplement added for donations to running events.";
                break;
            case (int)GenericEntities.EventType.Swimming:
                result = "\n 3% supplement added for donations to swimming events.";
                break;
            case (int)GenericEntities.EventType.Others:
                result = "\n No supplement should be applied for other events.";
                break; 
        }

        return result;
    }


   /// <summary>
   /// Calculate event supplement amount.
   /// </summary>
   /// <param name="eventCode"></param>
   /// <param name="donationAmount"></param>
   /// <returns></returns>
   public static decimal GetEventSupplementAmount(int eventCode, decimal donationAmount)
   {
       decimal result = 0;
       decimal eventSupplement = 0;

       //validation for invalid event code.
       if (eventCode > 2 || eventCode < 0) { eventCode = 2; }


       switch (eventCode)
       {
           case (int)GenericEntities.EventType.Running:
               eventSupplement = 5;
               break;
           case (int)GenericEntities.EventType.Swimming:
               eventSupplement = 3;
               break;
           case (int)GenericEntities.EventType.Others:
               eventSupplement = 0;
               break;
       }
       result = donationAmount*(eventSupplement/100);

       return result;
   }
}
}

Business Object

using GiftAidCalculator.TestConsole.Interface;


namespace GiftAidCalculator.TestConsole.BusinessObject
{
public class TaxCalculator: ITaxCalculator
{
    private readonly IConfigRepositry _configRepositry;

    public TaxCalculator(IConfigRepositry configRepositry)
    {
        _configRepositry = configRepositry;
    }


    public decimal CalculateGiftAid(decimal donationAmount)
    {
        var giftAidRatio = _configRepositry.TaxRate / (100 - _configRepositry.TaxRate);
        return donationAmount * giftAidRatio;
    }


}
}

Helper

using System;

namespace GiftAidCalculator.TestConsole.Helper
{
public  static class MathHelper
{
    //This can also be set as configurable member.
    private const int DecimalPoints = 2;


    public static decimal RoundDecimal(decimal giftAidAmount)
    {
        return decimal.Round(giftAidAmount, DecimalPoints, MidpointRounding.AwayFromZero);
    }
}
}


namespace GiftAidCalculator.TestConsole.Helper
{
public class ValidateHelper
{

    public static bool ValidateDecimal(string strInput, out decimal dOutput)
    {
        dOutput = 0;
        bool result = decimal.TryParse(strInput, out dOutput);
        return result;
    }

    public static bool ValidateInteger(string strInput, out int dOutput)
    {
        dOutput = 0;
        bool result = int.TryParse(strInput, out dOutput);
        return result;
    }
}
}

Entities

namespace GiftAidCalculator.TestConsole.Entities
{
class GenericEntities
{
    public enum EventType
    {
        Running =1,
        Swimming = 2,
        Others = 3
    }
}
}

UnitTest - As_event_promoter

using System;
using GiftAidCalculator.TestConsole.Repositry;
using NUnit.Framework;

namespace GiftAidCalculator.Tests
{
[TestFixture]
class As_event_promoter
{
    [Test]
    public void Add_5_percent_supplement_for_running()
    {
        //Arrange
        //Input one for running
        const int inputEvent = 1;
        const string expected = "\n 5% supplement added for donations to running events.";

        //Act
        string actual = EventRepositry.GetEventTypeMessage(inputEvent);

        //Assert
        Assert.AreEqual(actual,expected);
        Console.WriteLine(actual);
    }


    [Test]
    public void Add_3_percent_supplement_for_swimming()
    {
        //Arrange
        //Input one for swimming
        const int inputEvent = 2;
        const string expected = "\n 3% supplement added for donations to swimming events.";

        //Act
        string actual = EventRepositry.GetEventTypeMessage(inputEvent);

        //Assert
        Assert.AreEqual(actual, expected);
        Console.WriteLine(actual);
    }


    [Test]
    public void No_supplement_for_others()
    {
        //Arrange
        //Input three for others
        const int inputEvent = 3;
        const string expected = "\n No supplement should be applied for other events.";

        //Act
        string actual = EventRepositry.GetEventTypeMessage(inputEvent);

        //Assert
        Assert.AreEqual(actual, expected);
        Console.WriteLine(actual);
    }

    [Test]
    public void Show_event_supplement_amount_for_running()
    {
        //Arrange
        const int eventCode = 1; //running
        const decimal donationAmount = 45.56m;
        const decimal expected = 2.278m;

        //Act
        decimal actual = EventRepositry.GetEventSupplementAmount(eventCode, donationAmount);


        //Assert
        Assert.AreEqual(actual,expected);
        Console.WriteLine("Calulated supplement amount for running event with donation amount '" + donationAmount + "' will be : " + actual);
    }

}
}

UnitTest - As_site_administrator

using System;
using GiftAidCalculator.TestConsole.Interface;
using GiftAidCalculator.TestConsole.Repositry;
using NUnit.Framework;

namespace GiftAidCalculator.Tests
{
 [TestFixture]
class As_site_administrator
{
    [Test]
    public void Get_me_current_tax_rate()
    {
        //Arrange
        const decimal setCurrentTaxRate = 15.5m;
        const decimal expectedCurrentTaxRate = 15.5m;

        IConfigRepositry configRepositry = new ConfigRepositry();

        //Act
        configRepositry.TaxRate = setCurrentTaxRate;
        decimal actualCurrentTaxRate = configRepositry.TaxRate;

        //Assert
        Assert.AreEqual(actualCurrentTaxRate,expectedCurrentTaxRate);
        Console.WriteLine("Current Tax Rate ["+ actualCurrentTaxRate + " %] is set and retrieved from data store.");
    }
}
}

UnitTest - As_site_administrator

using System;
using GiftAidCalculator.TestConsole.Interface;
using GiftAidCalculator.TestConsole.Repositry;
using NUnit.Framework;

namespace GiftAidCalculator.Tests
{
[TestFixture]
class As_site_administrator
{
    [Test]
    public void Get_me_current_tax_rate()
    {
        //Arrange
        const decimal setCurrentTaxRate = 15.5m;
        const decimal expectedCurrentTaxRate = 15.5m;

        IConfigRepositry configRepositry = new ConfigRepositry();

        //Act
        configRepositry.TaxRate = setCurrentTaxRate;
        decimal actualCurrentTaxRate = configRepositry.TaxRate;

        //Assert
        Assert.AreEqual(actualCurrentTaxRate,expectedCurrentTaxRate);
        Console.WriteLine("Current Tax Rate ["+ actualCurrentTaxRate + " %] is set and retrieved from data store.");
    }
}
}

UnitTest - Gift_aid_should

using System;
using GiftAidCalculator.TestConsole.BusinessObject;
using GiftAidCalculator.TestConsole.Helper;
using GiftAidCalculator.TestConsole.Interface;
using Moq;
using NUnit.Framework;

namespace GiftAidCalculator.Tests
{
[TestFixture]
class Gift_aid_should
{

    private decimal CalculateGiftAid()
    {
        const decimal currectTaxrate = 15.65m;
        const decimal donationAmount = 50.50m;

        var mockConfigRepositry = new Mock<IConfigRepositry>();
        mockConfigRepositry.Setup(x => x.TaxRate).Returns(currectTaxrate);
        ITaxCalculator taxCalculator = new TaxCalculator(mockConfigRepositry.Object);

        return taxCalculator.CalculateGiftAid(donationAmount);
    }

    [Test]
    public void Be_rounded_to_two_decimal_digits()
    {
        //Arrange
        const decimal expectedValue = 9.37m;
        var calculatedGiftAid = CalculateGiftAid();

        //Act
        decimal actualValue = MathHelper.RoundDecimal(calculatedGiftAid);

        //Assert
        Assert.AreEqual(actualValue,expectedValue);
        Console.WriteLine("Actual calculated gift aid was [ " + calculatedGiftAid + " ] which is rounded to 2 decimal points : " + actualValue);


    }
}
}

UnitTest - Validate

using GiftAidCalculator.TestConsole.Helper;
using NUnit.Framework;
namespace GiftAidCalculator.Tests
{
[TestFixture]
class Validate
{
    [Test]
    public void Invalid_decimal_input()
    {
        //Arrange
        const decimal expected = 0;
        decimal actual = 0; 

        //Act
        ValidateHelper.ValidateDecimal("invalid", out actual);

        //Assert
        Assert.AreEqual(actual,expected);
    }


    [Test]
    public void Valid_decimal_input()
    {
        //Arrange
        const decimal expected = 24.536m;
        decimal actual = 0;

        //Act
        ValidateHelper.ValidateDecimal("24.536", out actual);

        //Assert
        Assert.AreEqual(actual, expected);
    }


    [Test]
    public void Invalid_integer_input()
    {
        //Arrange
        const int expected = 0;
        int actual = 0;

        //Act
        ValidateHelper.ValidateInteger("invalid", out actual);

        //Assert
        Assert.AreEqual(actual, expected);
    }


    [Test]
    public void Valid_integer_input()
    {
        //Arrange
        const int expected = 145;
        int actual = 0;

        //Act
        ValidateHelper.ValidateInteger("145", out actual);

        //Assert
        Assert.AreEqual(actual, expected);
    }
}
}

UnitTest - When_calculating_gift_aid

using System;
using GiftAidCalculator.TestConsole.BusinessObject;
using GiftAidCalculator.TestConsole.Interface;
using Moq;
using NUnit.Framework;

namespace GiftAidCalculator.Tests
{
[TestFixture]
class When_calculating_gift_aid
{

    [Test]
    public void Gift_aid_should_be_calculated_by_current_tax_rate()
    {
        //Arrange
        const decimal currectTaxrate = 20;
        const decimal donationAmount = 50;
        const decimal expectedResult = 12.5m;

        var mockConfigRepositry = new Mock<IConfigRepositry>();
        mockConfigRepositry.Setup(x => x.TaxRate).Returns(currectTaxrate);
        ITaxCalculator taxCalculator = new TaxCalculator(mockConfigRepositry.Object);

        //Act
        decimal actualResult = taxCalculator.CalculateGiftAid(donationAmount);

        //Assert
        Assert.AreEqual(expectedResult,actualResult);
        Console.WriteLine("Gift aid calculated at a tax rate of " + currectTaxrate + " is: " + actualResult);
    }

}
}
\$\endgroup\$
1
  • 1
    \$\begingroup\$ You should probably post the stories here, since, like the code, they seem to be critical to doing a review based on your requirements. \$\endgroup\$ – Ben Aaronson May 1 '14 at 23:42
3
\$\begingroup\$

class Program.

That's a class. A very high-level abstract notion of something with an entry point - a Main method. "Program" could be literally anything.

Writing SOLID code implies SRP, OCP, LSP, ISP and DIP.


public class ValidateHelper

Uh-oh. Red flag. Helper smells. Let's see...

public static bool ValidateDecimal(string strInput, out decimal dOutput)
public static bool ValidateInteger(string strInput, out int dOutput)

strInput and dOutput ring the Hungarian Notation bell. And there's a Copy+Paste error, I'm guessing dOutput in ValidateInteger was meant to be named iOutput... How about this?

public static bool ValidateDecimal(string value, out decimal result)
public static bool ValidateInteger(string value, out int result)

I'm only addressing the naming here though.

The main issue I'm seeing, is that the abstraction level at the entry point is very low, but in a SOLID app it's very high.

And the ending isn't very elegant either, and pretty much fails at DRY:

                Console.WriteLine("\n Press any key to exit.");
                Console.ReadLine();
            }
            else
            {
                Console.WriteLine("Invalid Input");
                Console.ReadLine();
            }
        }
        else
        {
            Console.WriteLine("Invalid Input");
            Console.ReadLine();
        }
    }
    else
    {
        Console.WriteLine("Invalid Input");
        Console.ReadLine();
    }

Try writing your application top-down. Take the time to put your mindset at the abstraction level that's right for the code you're writing. With the wrong abstraction levels, it's hard to make units for unit testing.

class Program
{
    static void Main()
    {
        IDataService dataService = new SomeDataService();
        IUserInteractionService interactionService = new ConsoleInteractionService();

        var app = new CalculatorApp(dataService, interactionService);
        app.Execute();
    }
}

SRP

The Main() method does one thing: it composes the application, and runs it. The composition is done with Poor Man's DI instead of with an IoC container, but refactoring to use one would be very easy.

OCP

Being static, the Main() method cannot be extended. The more code we write here, the more reasons to change we introduce, the further we stray from being open for extension, closed for modification. Writing code at the proper abstraction level helps succeeding here.

LSP

Code against abstractions, code against the interface of objects - don't assume you know what the actual runtime type is going to be (see the "typical violation" section here). In other words, SomeDataService could very well be fetching its data from async calls to a web service that returns JSON or XML data - Program doesn't need to know that much (abstraction levels again).

ISP

Interface Segregation boils down to the fact that modifying an interface is a breaking change. Observe the KISS principle here: the interface of a CalculatorApp object only exposes a parameterless Execute method. It's unlikely that anything else is ever going to be needed, because all the entry point ever needs to be doing is everything it takes to run this Execute method.

DIP

Dependency Inversion means that instead of newing up dependencies, a class will be injected with implementations for its dependencies, preferably through its constructor. Because CalculatorApp requires implementations for IDataService and IUserInteractionService, you already know that this class' responsibility will be to coordinate the interactions between these services, implementing the business logic.

In order to run the Execute method, Main has to instantiate a CalculatorApp. Because I'm injecting abstractions, I can write a unit test suite to cover everything the business rules dictate, where I'm free to inject a mock implementation for every dependency and verify that method X or Y is called N times on the IDataService, or whatever.


By building the app top-down by writing the abstractions first (actually with TDD, you'll first write a failing unit test) and peeling off abstraction levels as you dive into the low-level Console.WriteLine calls, you decide how much abstraction is enough abstraction, and it gets easier to realize how much abstraction is outright over-engineered: write only the code you need to write for tests to pass. Writing the app bottom-up in Main and refactoring towards SOLID makes this much harder than expanding the idea as you go from one level of abstraction to the next.

\$\endgroup\$
1
  • \$\begingroup\$ Mat that was very helpful. Much appreciated. \$\endgroup\$ – SAM May 2 '14 at 8:50
1
\$\begingroup\$

I don't like your switch code...

switch (eventCode)
{
    case (int)GenericEntities.EventType.Running:
        result = "\n 5% supplement added for donations to running events.";
        break;
    case (int)GenericEntities.EventType.Swimming:
        result = "\n 3% supplement added for donations to swimming events.";
        break;
    case (int)GenericEntities.EventType.Others:
        result = "\n No supplement should be applied for other events.";
        break; 
}

I think it's better idea to operate on objects.

using System;
class Program
{
    static void Main(string[] args)
    {
        var chosenEvent = Event.CreateEvent(1);

        Console.WriteLine(chosenEvent.GetEventTypeMessage());
        Console.WriteLine(chosenEvent.GetSupplementAmount(1000));

        Console.ReadLine();
    }
}

class Event
{
    private readonly string _name;
    private readonly decimal _suplement;

    public decimal GetSupplementAmount(decimal donationAmmount)
    {
        return _suplement * donationAmmount;
    }

    public string GetEventTypeMessage()
    {
        return String.Format("{0}% suplement added for donations to {1} events.", _suplement * 100, _name);
    }

    public static Event CreateEvent(int id)
    {
        if (id == 1)
            return new Event("running", 0.05m);
        else if (id == 2)
            return new Event("swimming", 0.03m);
        else if (id == 3)
            return new Event("other", 0.0m);
        else
            throw new IndexOutOfRangeException();
    }

    private Event(string name, decimal suplement)
    {
        _name = name;
        _suplement = suplement;
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.