Lesson Tuesday

When we think about our own individual to do lists, there are usually different types of tasks on that list. For instance, there are to dos that we need to complete for Epicodus, like our C# homework. There are chores at home such as washing dishes or mowing the lawn. There are likely other miscellaneous tasks, too, such as writing someone a birthday card.

Let's update our to do list application to allow users to organize their tasks by type. Over the next several lessons, we'll create a Category class. Each Category object will represent and store different categories of to do list Items such as "Work", "Home", and "School." This setup is commonly referred to as objects within objects.

Creating a Parent Class


Let's start by creating a Category class in a new ToDoList/Models/Category.cs file. It will also need a corresponding test file at ToDoList.Tests/ModelTests/CategoryTests.cs. The updated project structure looks like this:

ToDoList.Solution
├── ToDoList
│   ├── Controllers
│   │   ├── HomeController.cs
│   │   └── ItemsController.cs
│   ├── Models
│   │   ├── Category.cs
│   │   └── Item.cs
│   ├── Program.cs
│   ├── Startup.cs
│   ├── ToDoList.csproj
│   └── Views
│       ├── Home
│       │   └── Index.cshtml
│       └── Items
│           ├── DeleteAll.cshtml
│           ├── Index.cshtml
│           ├── New.cshtml
│           └── Show.cshtml
└── ToDoList.Tests
    ├── ModelTests
    │   ├── CategoryTests.cs
    │   └── ItemTests.cs
    └── ToDoList.Tests.csproj

In our new file, we'll do the following: declare a namespace and class, import the System.Collections.Generic library to use Lists, and declare properties.

ToDoList/Models/Category.cs
using System.Collections.Generic;

namespace ToDoList.Models
{
  public class Category
  {
    private static List<Category> _instances = new List<Category> {};
    public string Name { get; set; }
    public int Id { get; }
    public List<Item> Items { get; set; }
  }
}
  • _instances will contain a static List of all Category objects, similar to the _instances property we're currently using in the Item class.

  • Name will contain a name for the Category of tasks.

  • Id will contain a unique ID number that will be assigned in the constructor, similar to the Id we implemented in the Item class.

  • Items will contain a List of all Item objects that belong to that Category. For instance, if we had a Category with a Name of "chores," this list would contain a series of Item objects with Descriptions like "mop the floor", "scrub the shower", or "do the dishes."

  • Note that we're using an auto-implemented property with Items and declaring the data type as a List of Items.

It's time to add a constructor. We'll start with a test first. Let's configure our new test file with standard boilerplate code:

ToDoList.Tests/ModelTests/CategoryTests.cs
using Microsoft.VisualStudio.TestTools.UnitTesting;
using ToDoList.Models;
using System.Collections.Generic;
using System;

namespace ToDoList.Tests
{
  [TestClass]
  public class CategoryTests
  {

  }
}

Next, we'll add a test to confirm our constructor can successfully create Category objects:

ToDoList.Tests/ModelTests/CategoryTests.cs
using Microsoft.VisualStudio.TestTools.UnitTesting;
using ToDoList.Models;
using System.Collections.Generic;
using System;

namespace ToDoList.Tests
{
  [TestClass]
  public class CategoryTests
  {

    [TestMethod]
    public void CategoryConstructor_CreatesInstanceOfCategory_Category()
    {
      Category newCategory = new Category("test category");
      Assert.AreEqual(typeof(Category), newCategory.GetType());
    }

  }
}

Let's continue with our logic and add the constructor so that we run this test and see it pass:

ToDoList/Models/Category.cs
using System.Collections.Generic;

namespace ToDoList.Models
{
  public class Category
  {
    private static List<Category> _instances = new List<Category> {};
    public string Name { get; set; }
    public int Id { get; }
    public List<Item> Items { get; set; }

    public Category(string categoryName)
    {
      Name = categoryName;
      _instances.Add(this);
      Id = _instances.Count;
      Items = new List<Item>{};
    }
  }
}
  • The constructor only accepts an argument for categoryName, which is assigned to the Name property. All other properties are assigned automatically in the body of the constructor.

  • We add each Category to the static _instances list in the constructor when it's created.

  • We assign an Id number equal to the number of Categorys in _instances.

  • We create a new empty List to eventually contain Item objects that belong to this Category.

Next we'll add several methods and their corresponding tests. All of this should be review, so we'll go quickly.

First, let's test that a Category can successfully retrieve its name. We'll add a test and watch it pass thanks to our constructor code:

ToDoList.Tests/ModelTests/CategoryTests.cs
...

  [TestMethod]
  public void GetName_ReturnsName_String()
  {
    //Arrange
    string name = "Test Category";
    Category newCategory = new Category(name);

    //Act
    string result = newCategory.Name;

    //Assert
    Assert.AreEqual(name, result);
  }

...

Next, we'll test that we can retrieve Category IDs:

ToDoList.Tests/ModelTests/CategoryTests.cs
...

  [TestMethod]
  public void GetId_ReturnsCategoryId_Int()
  {
    //Arrange
    string name = "Test Category";
    Category newCategory = new Category(name);

    //Act
    int result = newCategory.Id;

    //Assert
    Assert.AreEqual(1, result);
  }

...

However, if we run our tests, they don't pass. We get a failure message:

Error Message:
 Assert.AreEqual failed. Expected:<1>. Actual:<3>.
Stack Trace:
   at ToDoList.Tests.CategoryTests.GetId_ReturnsCategoryId_Int() in ToDoList.Solution/ToDoList.Tests/ModelTests/CategoryTests.cs:line 44

It says we expected to receive 1 but got an ID of 3 instead. This is because we assign each Category Id by running Id = _instances.Count; in the constructor. The third test is receiving a Category with an Id of 3 because sample Categorys created in previous tests remain in the static _instances list.

We can fix this by disposing of all Categorys between tests with a teardown method similar to the one we implemented in our Item tests last section. We'll update the top of our CategoryTests class like this:

ToDoList.Tests/ModelTests/CategoryTests.cs
...

namespace ToDoList.Tests
{
  [TestClass]
  public class CategoryTests : IDisposable
  {

    public void Dispose()
    {
      Category.ClearAll();
    }

    ...
  ...
...

Next, we'll define this ClearAll() method:

ToDoList/Models/Category.cs
...

  public static void ClearAll()
  {
    _instances.Clear();
  }

...

After these changes, all tests should pass.

Moving on, we know we'll also need functionality to retrieve all Category objects to display in our app. Let's add that next. We'll start with a test:

ToDoList.Tests/ModelTests/CategoryTests.cs
...

  [TestMethod]
  public void GetAll_ReturnsAllCategoryObjects_CategoryList()
  {
    //Arrange
    string name01 = "Work";
    string name02 = "School";
    Category newCategory1 = new Category(name01);
    Category newCategory2 = new Category(name02);
    List<Category> newList = new List<Category> { newCategory1, newCategory2 };

    //Act
    List<Category> result = Category.GetAll();

    //Assert
    CollectionAssert.AreEqual(newList, result);
  }

...

Verify that it fails (it should throw a compiler error, since the method is not defined yet) and then add the static method to make it pass:

ToDoList/Models/Category.cs
...

    public static List<Category> GetAll()
    {
      return _instances;
    }

...

We also know we'll want a Find() method to locate and display specific Category objects. First, a test:

ToDoList.Tests/ModelTests/CategoryTests.cs
...

  [TestMethod]
  public void Find_ReturnsCorrectCategory_Category()
  {
    //Arrange
    string name01 = "Work";
    string name02 = "School";
    Category newCategory1 = new Category(name01);
    Category newCategory2 = new Category(name02);

    //Act
    Category result = Category.Find(2);

    //Assert
    Assert.AreEqual(newCategory2, result);
  }

...

Here's the logic to pass this test:

ToDoList/Models/Category.cs
...

    public static Category Find(int searchId)
    {
      return _instances[searchId-1];
    }

...

Notice this method is quite similar to our Item class Find() method. It accepts an ID as an argument and then locates the Category in the static _instances array that matches.

After following along with all steps in this lesson, all of our tests will pass and the new Category class will look like this:

ToDoList/Models/Category.cs
using System.Collections.Generic;

namespace ToDoList.Models
{
  public class Category
  {
    private static List<Category> _instances = new List<Category> {};
    public string Name { get; set; }
    public int Id { get; }
    public List<Item> Items { get; set; }

    public Category(string categoryName)
    {
      Name = categoryName;
      _instances.Add(this);
      Id = _instances.Count;
      Items = new List<Item>{};
    }

    public static void ClearAll()
    {
      _instances.Clear();
    }

    public static List<Category> GetAll()
    {
      return _instances;
    }

    public static Category Find(int searchId)
    {
      return _instances[searchId-1];
    }
  }
}

The corresponding test file looks like this:

ToDoList.Tests/ModelTests/CategoryTests.cs
using Microsoft.VisualStudio.TestTools.UnitTesting;
using ToDoList.Models;
using System.Collections.Generic;
using System;

namespace ToDoList.Tests
{
  [TestClass]
  public class CategoryTests : IDisposable
  {

    public void Dispose()
    {
      Category.ClearAll();
    }

    [TestMethod]
    public void CategoryConstructor_CreatesInstanceOfCategory_Category()
    {
      Category newCategory = new Category("test category");
      Assert.AreEqual(typeof(Category), newCategory.GetType());
    }

    [TestMethod]
    public void GetName_ReturnsName_String()
    {
      //Arrange
      string name = "Test Category";
      Category newCategory = new Category(name);

      //Act
      string result = newCategory.Name;

      //Assert
      Assert.AreEqual(name, result);
    }

    [TestMethod]
    public void GetId_ReturnsCategoryId_Int()
    {
      //Arrange
      string name = "Test Category";
      Category newCategory = new Category(name);

      //Act
      int result = newCategory.Id;

      //Assert
      Assert.AreEqual(1, result);
    }

    [TestMethod]
    public void GetAll_ReturnsAllCategoryObjects_CategoryList()
    {
      //Arrange
      string name01 = "Work";
      string name02 = "School";
      Category newCategory1 = new Category(name01);
      Category newCategory2 = new Category(name02);
      List<Category> newList = new List<Category> { newCategory1, newCategory2 };

      //Act
      List<Category> result = Category.GetAll();

      //Assert
      CollectionAssert.AreEqual(newList, result);
    }

    [TestMethod]
    public void Find_ReturnsCorrectCategory_Category()
    {
      //Arrange
      string name01 = "Work";
      string name02 = "School";
      Category newCategory1 = new Category(name01);
      Category newCategory2 = new Category(name02);

      //Act
      Category result = Category.Find(2);

      //Assert
      Assert.AreEqual(newCategory2, result);
    }
  }
}

We've set up our new Category class with basic functionality that's thoroughly tested. In the next lesson, we'll build it out further so we can save Item objects within Category objects.

Repository Reference

Follow the link below to view how a sample version of the project should look at this point. Note that this is a link to a specific commit in the repository.

Example GitHub Repo for To Do List

Objects within objects: The process of storing one object inside another object.

Lesson 4 of 11
Last updated more than 3 months ago.