mercredi 20 janvier 2021

C# - Menu Implementation - Help writing it in a testable way

The Question:

I'm developing an app in C# and am currently designing a menu which contains submenus and needs to be different depending on the category of the user.

I've written the code but am struggling to structure it so that it is testable.

I've read a lot about unit testing and testable code but still find it tough to write it well - I was hoping people could give me some pointers on re-structuring so that it's testable and so I'm not setting myself up for trouble down the road.

The Code:

A menu item:

public struct Item
{
    public string HRef { get; set; }
    public string Title { get; set; }

    public Item(string hRef, string title)
    {
        HRef = hRef;
        Title = title;
    }
}

A menu:

It has a list of items and a list of submenus (I took inspiration from how a directory tree is structured with files and folders)

public class Menu
{
    public string Name { get; set; }
    public List<Item> Items { get; set; }
    public List<Menu> SubMenus { get; set; }

    public Menu(string name, List<Item> items, List<Menu> subMenus)
    {
        Name = name;
        Items = items;
        SubMenus = subMenus;
    }
}

The Menu Manager: - The thing under test

The menu manager contains a repository where I get the category of a user from

It has a GetMenu() method which accepts the user name and returns the correct menu depending on the user's category.

It has two private methods which build the menu for each category (currently just Admin and Default but will eventually be more).

public class Manager
    {
        WorkflowPlusApp.Database.IInfoStaffRepository repository;

        public Manager(IInfoStaffRepository repository)
        {
            this.repository = repository;
        }

        public Menu GetMenu(string userName)
        {
            var userCategory = repository.GetStaffCategory(userName);

            switch(userCategory)
            {
                case "Admin": return GetAdminMenu();
                
                default: return GetDefaultMenu();
            }
        }




        private Menu GetAdminMenu()
        {
            return new Menu(
                name: "/",
                items: new List<Item>()
                {
                    new Item(hRef: "", title: "Request Form"),
                    new Item(hRef: "", title: "Tasks"),
                    new Item(hRef: "", title: "Project"),
                },
                subMenus: new List<Menu>()
                {
                    new Menu(
                        name: "/Config",
                        items: new List<Item>()
                        {
                            new Item(hRef: "", title: "Area")
                        },
                        subMenus: new List<Menu>())
                });
        }




        private Menu GetDefaultMenu()
        {
            return new Menu(
                name: "/",
                items: new List<Item>()
                {
                    new Item(hRef: "", title: "Request Form"),                    
                },
                subMenus: new List<Menu>());
        }
    }

The Unit Test

I've written a unit test using MSTest and Moq

[TestMethod()]
        public void GetMenu_Staff_Admin_Returns_Admin_Menu()
        {
            var userName = "TestUser";

            //Setup Mock
            var mockRepository = new Mock<WorkflowPlusApp.Database.IInfoStaffRepository>();
            mockRepository.Setup(x => x.GetStaffCategory(userName)).Returns("Admin");
            
            //Setup
            var manager = new Menus.Manager(mockRepository.Object);
            var menu = manager.GetMenu(userName);

            //Test - Does it return the Admin menu?
            Assert.IsTrue(menu.Items.Count == 3);
            Assert.IsTrue(menu.Items[0].Title == "Request Form");
            Assert.IsTrue(menu.Items[0].Title == "Tasks");
            Assert.IsTrue(menu.Items[0].Title == "Project");
            Assert.IsTrue(menu.SubMenus.Where(x => x.Name == "Config").ToList()[0].Items.Count == 1);
            Assert.IsTrue(menu.SubMenus.Where(x => x.Name == "Config").ToList()[0].Items[0].Title == "Area");
        }

The Admin menu should be returned in a particular order and have particular entries so I thought by using the Titles I'm testing a behaviour - if the menu changes it should fail and alert me that the menu isn't right for Admin staff.

I can't help feeling like this test is tied to much to the implementation and is bad code though.

In setting up the Mock I have to know two things:

That the Manager's GetMenu() function calls the repository's GetStaffCategory() function

That the category the repository returns is Admin

So I'm tying myself to two of the Manager's implementation details - if they change I'll have to change the test code

I would love to be able to write code well and while the books and articles I've read are helpful I still find it difficult to do in practice, any help or suggestions - even if it's scrap the whole thing and use a simple Dictionary - would be helpful towards me learning to write testable decent code.

Many Thanks

Nick

Aucun commentaire:

Enregistrer un commentaire