Building on a Foundation: C# Inheritance

You've just mastered encapsulation, a crucial technique for creating robust, self-contained objects. Now, let's tackle another major pillar of Object-Oriented Programming that promotes elegant code reuse: Inheritance.

Imagine you're building out our application. You'll likely have different types of users: a StandardUser, a GuestUser with limited access, and an AdminUser with special powers. All these user types share common properties and behaviors – they all have a username, they all can log in. It would be incredibly repetitive to write that same code in three separate classes.

Inheritance provides a powerful solution, allowing us to create a base blueprint and have more specialized blueprints inherit from it. Let's learn how to build on the foundations we've already laid. 🏛️

Tommy and Gina looking at three similar robot builds having the same bodies but different types of legs: robotic legs, wheels, and tracks

What is Inheritance?

Inheritance is a fundamental mechanism in OOP that allows a new class to acquire, or "inherit", the properties and behaviors (methods) of an existing class. This creates a parent-child relationship between classes.

  • The existing class is called the base class (or parent, or superclass).
  • The new class that inherits from it is called the derived class (or child, or subclass).

This creates a clear "is-a" relationship. For example, a GoldenRetriever is a Dog. A Dog is an Animal. The GoldenRetriever class would inherit all the common behaviors of the Dog class (like a Bark() method), which in turn inherits from the Animal class (like an Eat() method). The GoldenRetriever can then add its own unique behaviors, like RetrieveStick(). The primary benefits of this are code reuse and the ability to create logical, easy-to-understand class hierarchies.

Instead of copy-pasting code, you define common functionality once in a base class, and let multiple derived classes inherit it automatically.

Defining Base and Derived Classes in C#

The syntax for inheritance in C# is simple and elegant: you use a colon (:) after the derived class's name, followed by the name of the base class.

public class DerivedClassName : BaseClassName

Let's build on our UserAccount idea. First, we'll create a base UserAccount class with the properties and methods that all users share.

// The Base (or Parent) Class
public class UserAccount
{
    public string Username { get; set; }
    public DateTime LastLoginDate { get; protected set; } // Protected set is interesting!
 
    public void Login()
    {
        LastLoginDate = DateTime.Now;
        Console.WriteLine($"User '{Username}' has logged in.");
    }
}   

Now, let's create two derived (child) classes that inherit from UserAccount.

// A Derived (or Child) Class
public class GuestUser : UserAccount
{
    // This class starts with everything UserAccount has!
    // We can add properties specific to guests.
    public int SessionDurationMinutes { get; set; }
}
 
// Another Derived Class
public class AdminUser : UserAccount
{
    // This class also gets everything from UserAccount.
    // Let's add an admin-specific property.
    public string AdminPermissionLevel { get; set; }
 
    // And an admin-specific method.
    public void DeleteUserAccount(string userToDelete)
    {
        Console.WriteLine($"Admin '{Username}' is deleting user '{userToDelete}'.");
    }
}   

Now, if we create an instance of AdminUser, it automatically has access to the Username property and the Login() method from its parent, UserAccount:

public static void Main(string[] args)
{
    AdminUser superUser = new AdminUser();
    superUser.Username = "SuperAdmin";
    superUser.AdminPermissionLevel = "Full Control";
 
    superUser.Login(); // Calling the method inherited from UserAccount!
    superUser.DeleteUserAccount("RogueUser"); // Calling its own method.
 
    Console.WriteLine($"{superUser.Username} has permission level: {superUser.AdminPermissionLevel}");
    Console.ReadLine();
}   

As you can see, we didn't have to rewrite the Username property or the Login() method for our AdminUser. We got it for free through inheritance!

Inheritance Scope – Access Modifiers Revisited

So what, exactly, does a child class inherit from its parent? The answer lies in the access modifiers we use.

  • public members of the base class are fully inherited and remain public in the derived class. This means they can be accessed by the derived class itself and by any external code.
  • private members of the base class are not directly accessible by the derived class. They are part of the object's memory, but they are hidden away, respecting the base class's encapsulation. The child class cannot "see" or touch its parent's private fields.

This leads to an important question: what if you want a member to be private to the outside world, but accessible to its child classes? For this, C# provides the protected access modifier.

The protected Access Modifier

Think of protected as the "for family members only" access level. A protected member is accessible within its own class and by any class that inherits from it, but it remains inaccessible to the outside world.

public class BaseTest
{
    // This field can be accessed by BaseTest and any class inheriting from it.
    protected string TestRunId;
 
    public BaseTest()
    {
        // Generate a unique ID for each test run instance
        TestRunId = Guid.NewGuid().ToString();
    }
}
 
public class LoginTests : BaseTest
{
    public void MyTestMethod()
    {
        // We can access the protected field from the parent class!
        Console.WriteLine($"Running a test with Run ID: {TestRunId}");
    }
}
 
// In some other unrelated class:
// LoginTests myTest = new LoginTests();
// myTest.TestRunId = "some-id"; // COMPILE ERROR! Cannot access protected member.    

The protected modifier is extremely useful for sharing common state or helper methods across a class hierarchy while still keeping them hidden from the general public.

Constructors and Inheritance

Here's a critical rule you must remember: a derived class constructor must always call a constructor of its base class. This makes sense, because before you can build the "child" part of an object, the "parent" part must be properly initialized first!

Implicit and Explicit Base Calls

If your base class has a public, parameterless constructor (like public UserAccount() { }), the C# compiler will automatically and implicitly add a call to it from your derived class's constructor. You don't have to write anything.

However, if your base class only has constructors that require parameters, your derived class must explicitly call one of them. You do this using the : base() keyword syntax right after your derived class's constructor signature.

Let's update our UserAccount to require a username upon creation:

public class UserAccount
{
    public string Username { get; private set; } // Can only be set internally
 
    // The ONLY constructor requires a username
    public UserAccount(string username)
    {
        if (string.IsNullOrWhiteSpace(username))
        {
            throw new ArgumentException("Username cannot be empty.");
        }
        Username = username;
        Console.WriteLine($"UserAccount base for '{Username}' constructed.");
    }
    // No parameterless constructor exists anymore!
}
 
public class AdminUser : UserAccount
{
    public string PermissionLevel { get; set; }
 
    // We MUST call the base constructor and pass the username up the chain.
    public AdminUser(string username, string permissionLevel) : base(username)
    {
        // The base(username) part runs FIRST, initializing the UserAccount part.
        // Then, the code inside these curly braces runs.
        PermissionLevel = permissionLevel;
        Console.WriteLine($"AdminUser derived part for '{Username}' constructed.");
    }
}   

When you create a new AdminUser:

AdminUser superUser = new AdminUser("SuperAdmin", "FullAccess");
// Output:
// UserAccount base for 'SuperAdmin' constructed.
// AdminUser derived part for 'SuperAdmin' constructed.

The base(username) call ensures the parent UserAccount constructor runs first, setting up the Username before the AdminUser constructor sets its own PermissionLevel.

Inheritance in Test Automation Frameworks

Inheritance isn't just an abstract concept; it's a massively practical tool for writing clean and maintainable test automation. One of the most common and powerful patterns you'll use is the Base Test Class.

The Base Test Class Pattern

The idea is to create a base class (e.g., BaseTest) that contains all the common setup and teardown logic that many of your test classes will need. For example:

  • Initializing the Selenium WebDriver and setting up browser options.
  • Reading configuration data (like URLs, usernames, passwords).
  • Logic to take a screenshot if a test fails.
  • Closing the browser and disposing of the WebDriver instance after a test runs.

Your specific test classes (like LoginTests, ProductSearchTests, etc.) can then simply inherit from this BaseTest class. By doing so, they automatically gain all that common setup and teardown functionality without having to repeat a single line of that code.

// A simplified example of a Base Test Class
public abstract class BaseTest // 'abstract' means you can't create an instance of BaseTest itself
{
    // protected IWebDriver driver; // The WebDriver instance, protected so child tests can access it
 
    [SetUp]
    public void TestSetup()
    {
        // Code to launch the Chrome browser and navigate to the login page
        Console.WriteLine("SETUP: Browser launched, navigated to site.");
    }
 
    [TearDown]
    public void TestTeardown()
    {
        // Code to check if the test failed, and if so, take a screenshot
        // Code to close the browser and clean up
        Console.WriteLine("TEARDOWN: Browser closed.");
        Console.WriteLine("-----------------------------");
    }
}
 
// A specific test class that inherits all the setup/teardown logic
[TestFixture]
public class LoginTests : BaseTest // Inherits from BaseTest!
{
    [Test]
    public void LoginWithValidCredentials_ShouldSucceed()
    {
        Console.WriteLine("Executing valid login test...");
        // Test logic here... we don't need to write browser setup/teardown!
        // It's automatically handled by the [SetUp] and [TearDown] from BaseTest.
        Assert.Pass(); // Just pass it for the example
    }
}   

The Power of a Base Test Class

Creating a base class for your tests is one of the single most effective things you can do to keep your automation framework DRY (Don't Repeat Yourself) and maintainable. When you need to change how the browser starts up (e.g., add a new Chrome option) or how failures are logged, you only have to change it in one place: your BaseTest class. All inheriting test classes will automatically get the new behavior.

This pattern is a cornerstone of professional test automation framework design.

A Note on Single Inheritance in C#

It's important to know one key rule about inheritance in C#: a class can only inherit directly from one base class. This is known as "single inheritance." You cannot write something like public class MyClass : BaseClass1, BaseClass2. This rule prevents certain complex problems (like the "Diamond Problem") found in other languages that allow multiple inheritance.

But what if you want your class to have behaviors from multiple different sources? For this, C# uses a different but related concept called Interfaces. An interface defines a "contract" of what a class can do without providing the implementation. A class can inherit from one base class but can implement many interfaces.

We'll dive into interfaces in a future lesson, but for now, just remember: one direct parent class per child class.

Key Takeaways

  • Inheritance is a core OOP principle allowing a new class (derived/child) to acquire the members of an existing class (base/parent), enabling code reuse and creating "is-a" relationships.
  • You implement inheritance in C# using a colon: public class ChildClass : ParentClass.
  • public and protected members of a base class are accessible in a derived class, while private members remain hidden, respecting encapsulation.
  • A derived class constructor must call a base class constructor. Use the : base() syntax to pass arguments to the parent's constructor explicitly.
  • The Base Test Class pattern is a highly effective use of inheritance in test automation to share common setup (e.g., launching a browser) and teardown logic across all your tests.
  • C# supports single inheritance for classes but allows implementing multiple interfaces (a topic for a later lesson).

Building on the Shoulders of Classes

What's Next?

You've now learned how to create class hierarchies and reuse code with inheritance, another massive step in your OOP journey! This ability for one object to "be" a type of another object unlocks the next powerful OOP pillar. Next, we'll explore Changing Forms: An Intro to Polymorphism in C#, where we'll see how you can treat different but related objects in a wonderfully uniform way.