Dog Exercise
Let's create a Unit Test project called "DogTests" and the solution "Dog".
Requirements
Let's see the requirements
A dog has:
- name (string)
- age (int)
- weight (double)
- color (enum: white, black and brown)
Deny name if empty or less than 3 chars. Deny age if less than 0 (can be several months). Deny weight if less than 0 (can be hundres of grams). Deny color if is not from enum.
Let's start with the Name
and Age
.
[TestMethod]
public void TestMethod1()
{
// arrange
var name = "Rex";
var age = 12;
// act
var dog = new Dog(name, age);
// assert
Assert.AreEqual(age, dog.Age);
Assert.AreEqual(name, dog.Name);
}
Generate the Dog
class and the properties.
internal class Dog
{
private string name;
private int age;
public Dog(string name, int age)
{
this.name = name;
this.age = age;
}
public int Age { get; internal set; }
public string Name { get; internal set; }
}
If we run the test, it fails because the backing fields are not connected to the properties. Let's fix them.
internal class Dog
{
public int Age { get; internal set; }
public string Name { get; internal set; }
public Dog(string name, int age)
{
Name = name;
Age = age;
}
}
Refactoring:
- make the
set
accessorprivate
for both properties - make the class
public
- rename the test method to
TestNameAge
.
private set
Encapsulation:
- you cannot change the value of this property outside this class.
- the property is not accessible for other classes.
When we create a new object of this class and we pass the arguments, then is the only time we have access to this property because the constructor of this class is able to access it being in the same class with the private property.
I choose to have the Color as a auto-property because you will set its value once and it will be the same during the lifetime of the object (a dog keeps its color) and we don't want external classes to change it.
Let's test the weight:
[TestMethod]
public void TestWeight()
{
// arrange
var weight = 5.4;
// act
// assert
}
As you can see, we need the variables from the first test method again, let's make them available to all test methods.
Dog dog;
string name;
int age;
double weight;
[TestInitialize]
public void Initialize()
{
name = "Rex";
age = 12;
weight = 5.4;
}
And here is the test method for the Weight
:
[TestMethod]
public void TestWeight()
{
// arrange
dog = new Dog(name, age);
// act
dog.Weight = weight;
// assert
Assert.AreEqual(weight, dog.Weight);
}
Let's generate the Weight
property.
public double Weight { get; internal set; }
And if we run the test, it works.
We can also leave out the internal from the set
.
Let's continue with the Color
.
[TestMethod]
public void TestColor()
{
// arrange
var color = Colors.White;
// act
// assert
}
Generate the Colors
enum and add the colors there.
Now, continue the test method:
[TestMethod]
public void TestColor()
{
// arrange
var color = Colors.White;
dog = new Dog(name, age);
// act
dog.Color = color;
// assert
Assert.AreEqual(color, dog.Color);
}
And generate the Color
property.
public Colors Color { get; set; }
Let's look now at the exceptions.
First, the Name
property.
So, we need to make sure that the value we pass for the Name
is not empty or null. We can do this directly in the Name
property.
One problem is that right now this property is an auto-property. We need to make it a full property to be able to add Validation.
But first, let's add the test method.
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void TestEmptyName()
{
// arrange
name = string.Empty;
// act
dog = new Dog(name, age);
// assert
}
Notice that I didn't add anything to the assert section. The ExpectedException
is part of it indirectly.
Now, go to the Name
property and press Ctrl + .
on it, then choose Convert to full property.
It splits it to:
private string _name;
public string Name { get => _name; private set => _name = value; }
The first one is the backing field or store field. And the second one is the actual property which preserves its value inside its associated backing field.
Naming
The backing field is always private so we name it with lowercase. The property is public so it starts with uppercase.
value
The value
keyword is used in public properties to represent the input/data that was passed in by the user.
Throw it, we get that information passed to the property.
Also, the type of the value
parameter always matches the type of the property.
We need to rewrite it a little bit:
public string Name
{
get => _name; private
set
{
_name = value;
}
}
And in the set
accessor we can check the value we get for the name
.
public string Name
{
get => _name; private
set
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentOutOfRangeException(nameof(Name), value, "Name cannot be empty");
}
_name = value;
}
}
And now the test passes 😆
This is called Data Validation.
Explanation
The backing field which stores the data can be changed if the value passes the conditions in the set assesor of the associtated property.
Here we didn't achieve Encapsulation because the property which operates on this backing field can be whenever called outside this class and it can change the data in the backing field.
So, if the condition/ contraint is not respected, then an exception will be thrown.
But the Name
must be at least 3
characters long as well.
Let's add a test method for that.
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void TestShortName()
{
// arrange
name = "RT";
// act
dog = new Dog(name, age);
// assert
}
If we run this, it fails.
Let's fix it:
public string Name
{
get => _name; private
set
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentOutOfRangeException(nameof(Name), value, "Name cannot be empty");
}
if (value.Length < 3)
{
throw new ArgumentOutOfRangeException(nameof(Name), value, "Name is too short");
}
_name = value;
}
}
And it works.
Let's do the same for the Age
.
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void TestNegativeAge()
{
// arrange
age = -8;
// act
dog = new Dog(name, age);
// assert
}
First, we need to convert it to a full property.
And then we can add an if
statement:
public int Age
{
get => _age;
private set
{
if(value < 0)
{
throw new ArgumentOutOfRangeException(nameof(Age), value, "Age cannot be below 0");
}
_age = value;
}
}
And this passes.
We have to do the exact same thing for the Weight
property:
- add a test method
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void TestNegativeWeight()
{
// arrange
weight = -8;
dog = new Dog(name, age);
// act
dog.Weight = weight;
// assert
}
convert to full property
add the condition to the validate the value
public double Weight
{
get => _weight;
set
{
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(Weight), value, "Weight cannot be below 0");
}
_weight = value;
}
}
And the Color
is already good because the enum
, in essence`, is a list of options and we cannot assign something else than what it is provided by the enum.
Done 🎆
Class library
We need to extract the Dog
class into a separate class library project.
In the Unit Test project, add reference to this class library.
Also, add this to use the Colors
enum:
using static DogLibrary.Dog;
Console project
Let's use the class.
We need to create a new console application called DogConsole.
Let's create a new dog:
var dog1 = new Dog("Lassie", 4); // the set accessors are invoked here.
Console.WriteLine(dog.Name); // the get accessors are invoked here.
Console.WriteLine(dog.Age);
We can run (don't forget to set the console application as the startup project).
Let's create a dog with wrong input values:
var dog3 = new Dog("TY", -2);
Console.WriteLine(dog3.ToString());
But this crashes the application because an exception is thrown.
We need to surround it with a try catch
block:
try
{
var dog3 = new Dog("TY", -2);
Console.WriteLine(dog3.ToString());
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
So, if exceptions are thrown, then we print the message on the screen. Usually we will do more things than this.
And that it all, folks! 😁
Check out the source code on GitHub.