আমি আপনার এক্সপার্ট সফটওয়্যার ইঞ্জিনিয়ারিং ট্রেইনার। কোর্স আউটলাইন অনুযায়ী, আমরা এখন Section 15: xUnit testing-এর ভেতরে আছি। আগের লেকচারে আমরা Person মডেল এবং DTO তৈরি করেছিলাম। এই লেকচারে আমরা TDD (Test Driven Development) অ্যাপ্রোচ ফলো করে AddPerson মেথডটির জন্য Unit Test লেখা শিখব।

চলুন, লেকচারটি গুছিয়ে এবং বিস্তারিতভাবে বুঝে নেওয়া যাক।

📝 Lecture Summary (Quick Revision)

ভবিষ্যতে দ্রুত রিভিশন দেওয়ার জন্য সম্পূর্ণ লেকচারের মূল কাজগুলো নিচে লিস্ট আকারে দেওয়া হলো:

  • Interface Setup: ServiceContracts প্রজেক্টে IPersonsService ইন্টারফেস তৈরি করে সেখানে AddPerson এবং GetAllPersons মেথড ডিক্লেয়ার করা।
  • Dummy Implementation: Services প্রজেক্টে PersonsService ক্লাস তৈরি করে ইন্টারফেসটি ইমপ্লিমেন্ট করা এবং মেথডগুলোতে সাময়িকভাবে NotImplementedException থ্রো করে রাখা।
  • Test Class Initialization: Test প্রজেক্টে PersonsServiceTest ক্লাস তৈরি করে কনস্ট্রাক্টরের মাধ্যমে PersonsService এর অবজেক্ট ইনিশিয়ালাইজ করা।
  • Test Case 1 (Null Request): ইনপুট হিসেবে null পাস করলে মেথডটি যেন ArgumentNullException থ্রো করে তা Assert.Throws দিয়ে চেক করা।
  • Test Case 2 (Null Property): ইনপুট অবজেক্টের PersonName প্রপার্টি null হলে যেন ArgumentException থ্রো করে তা টেস্ট করা।
  • Test Case 3 (Proper Insertion): সঠিক ডেটা দিলে মেথডটি যেন নতুন PersonID জেনারেট করে এবং GetAllPersons মেথড কল করলে রিটার্ন হওয়া লিস্টের ভেতর যেন নতুন অ্যাড করা ডেটাটি থাকে, তা Assert.Contains দিয়ে নিশ্চিত করা।
  • TDD Verification: টেস্টগুলো রান করে দেখা যে সবগুলো ফেইল করছে, কারণ মেথডগুলোতে এখনো আসল লজিক লেখা হয়নি।

🧠 Comprehensive Breakdown

এই লেকচারে আমরা মূলত xUnit ব্যবহার করে AddPerson মেথডের তিনটি ভিন্ন ভিন্ন সিনারিও (Scenario) টেস্ট করেছি। চলুন প্রতিটি ধাপ বিস্তারিতভাবে বুঝে নিই।

১. Interface এবং Service Class তৈরি করা (Priority: 8/10)

Concept & Why: Dependency Injection (DI) এবং Abstraction ঠিক রাখার জন্য সব সময় Service ক্লাসের আগে তার Interface তৈরি করতে হয়। IPersonsService ইন্টারফেসে আমরা দুটি মেথড ডিক্লেয়ার করেছি। ইনপুট প্যারামিটারকে Nullable (?) করা হয়েছে, যাতে ইউজার null পাস করলে আমরা কাস্টম এক্সেপশন থ্রো করে তা হ্যান্ডেল করতে পারি।

(VS / VS Code Shortcut: কোনো ইন্টারফেসের নাম লিখে তার ওপর কার্সর রেখে Ctrl + . চাপলে “Implement Interface” অপশন আসে, যা অটোমেটিক্যালি ক্লাসের ভেতর মেথডগুলো তৈরি করে দেয়।)

// ServiceContracts/IPersonsService.cs
public interface IPersonsService
{
    PersonResponse AddPerson(PersonAddRequest? personAddRequest);
    List<PersonResponse> GetAllPersons();
}
 
// Services/PersonsService.cs
public class PersonsService : IPersonsService
{
    public PersonResponse AddPerson(PersonAddRequest? personAddRequest)
    {
        throw new NotImplementedException(); // TDD এর জন্য ডামি কোড
    }
 
    public List<PersonResponse> GetAllPersons()
    {
        throw new NotImplementedException(); // TDD এর জন্য ডামি কোড
    }
}
 

২. Test Class Setup এবং Initialization (Priority: 9/10)

Concept & Why: টেস্টিং প্রজেক্টে PersonsServiceTest নামের একটি ক্লাস তৈরি করা হয়েছে। এই ক্লাসের কনস্ট্রাক্টরে PersonsService এর একটি ইনস্ট্যান্স (Instance) তৈরি করা হয়েছে। xUnit-এ প্রতিটি টেস্ট রান করার আগে কনস্ট্রাক্টর অটোমেটিক্যালি কল হয়, ফলে প্রতিটি টেস্ট একটি ফ্রেশ (Fresh) অবজেক্ট পায়।

public class PersonsServiceTest
{
    private readonly IPersonsService _personsService;
 
    public PersonsServiceTest()
    {
        // Service অবজেক্ট ইনিশিয়ালাইজ করা হচ্ছে
        _personsService = new PersonsService(); 
    }
}
 

(নোট: আমরা এখনো ডাটাবেস বা Mockিং শিখিনি, তাই এখানে সরাসরি Service-এর অবজেক্ট new কিওয়ার্ড দিয়ে তৈরি করা হয়েছে। কোর্সের Section 19-এ আমরা Moq লাইব্রেরি ব্যবহার করা শিখব)।

৩. Unit Test 1: Null Request Check (Priority: 10/10)

Concept & Why: ইউজার যদি ভুল করে PersonAddRequest এর বদলে সম্পূর্ণ null পাঠায়, তবে সিস্টেম ক্র্যাশ না করে যেন ArgumentNullException থ্রো করে, সেটি টেস্ট করা। xUnit-এ Exception টেস্ট করার জন্য Assert.Throws<TException>() ব্যবহার করা হয়।

[Fact]
public void AddPerson_NullPersonAddRequest()
{
    // Arrange
    PersonAddRequest? personAddRequest = null;
 
    // Act & Assert
    Assert.Throws<ArgumentNullException>(() => 
    {
        _personsService.AddPerson(personAddRequest);
    });
}
 

৪. Unit Test 2: Null Property Check (Priority: 10/10)

Concept & Why: যদি রিকোয়েস্ট অবজেক্টটি null না হয়, কিন্তু ইউজারের নাম (PersonName) না দেওয়া থাকে, তবে সেটি ইনভ্যালিড। এই অবস্থায় আমাদের Service-এর উচিত ArgumentException থ্রো করা।

[Fact]
public void AddPerson_PersonNameIsNull()
{
    // Arrange
    PersonAddRequest? personAddRequest = new PersonAddRequest()
    {
        PersonName = null, // নাম null দেওয়া হলো
        Email = "test@example.com"
    };
 
    // Act & Assert
    Assert.Throws<ArgumentException>(() => 
    {
        _personsService.AddPerson(personAddRequest);
    });
}
 

৫. Unit Test 3: Proper Data Insertion (Priority: 10/10)

Concept & Why: এটি সবচেয়ে গুরুত্বপূর্ণ পজিটিভ টেস্ট। আমরা সঠিক ডেটা দিয়ে AddPerson কল করব। এখানে দুটি জিনিস Assert বা যাচাই করতে হবে:

  1. রিটার্ন আসা PersonResponse এর ভেতরের PersonID যেন খালি (Empty Guid) না হয়।
  2. GetAllPersons() মেথড কল করলে যে লিস্ট আসবে, সেখানে যেন আমাদের নতুন তৈরি করা Person-টি উপস্থিত থাকে (Test Isolation এর কারণে লিস্টটি ডিফল্টভাবে ফাঁকা থাকে)।
[Fact]
public void AddPerson_ProperPersonDetails()
{
    // Arrange
    PersonAddRequest? personAddRequest = new PersonAddRequest()
    {
        PersonName = "Rahim",
        Email = "rahim@example.com",
        Gender = GenderOptions.Male,
        DateOfBirth = DateTime.Parse("2000-01-01"),
        CountryID = Guid.NewGuid(), // Dummy Guid
        ReceiveNewsLetters = true
    };
 
    // Act
    PersonResponse person_response_from_add = _personsService.AddPerson(personAddRequest);
    List<PersonResponse> persons_list = _personsService.GetAllPersons();
 
    // Assert 1: ID জেনারেট হয়েছে কিনা চেক করা
    Assert.True(person_response_from_add.PersonID != Guid.Empty);
 
    // Assert 2: লিস্টে অবজেক্টটি আছে কিনা চেক করা 
    // (এটি কাজ করার জন্য PersonResponse ক্লাসে Equals মেথড Override করা থাকতে হবে)
    Assert.Contains(person_response_from_add, persons_list);
}
 

৬. TDD (Test Driven Development) Verification (Priority: 7/10)

Concept: টেস্টগুলো লেখার পর Visual Studio এর Test Explorer থেকে রান করলে দেখা যাবে সবগুলো টেস্ট Fail করেছে। Why: কারণ আমরা এখনো AddPerson মেথডের আসল লজিক লিখিনি, সেখানে শুধু throw new NotImplementedException(); লেখা আছে। TDD-এর নিয়মই হলো: আগে টেস্ট লেখো -> টেস্ট ফেইল করো -> লজিক লেখো -> টেস্ট পাস করো। আগামী লেকচারে আমরা লজিক লিখে এই টেস্টগুলো পাস করাব।


🚀 Best Practices & Modern C# (.NET 10 Update)

1. Naming Convention for Tests: টেস্ট মেথডের নাম সব সময় MethodName_StateUnderTest_ExpectedBehavior প্যাটার্নে হওয়া উচিত। লেকচারার যেমন PersonNameNull লিখেছেন, এর চেয়ে ভালো প্র্যাকটিস হলো: AddPerson_PersonNameIsNull_ThrowsArgumentException। এতে টেস্ট ফেইল করলে নাম দেখেই বোঝা যায় সমস্যা কোথায়।

2. Modern Syntax for Assertion (.NET 8/10): Exception টেস্টিং-এর ক্ষেত্রে Assert.Throws এখনো স্ট্যান্ডার্ড। তবে Assert.True(personID != Guid.Empty) লেখার চেয়ে xUnit এর স্পেসিফিক মেথড ব্যবহার করা বেটার:

// Old
Assert.True(person_response_from_add.PersonID != Guid.Empty);
 
// Better and Modern
Assert.NotEqual(Guid.Empty, person_response_from_add.PersonID);
 

3. Arrange, Act, Assert (AAA Pattern): টেস্ট মেথডের ভেতরে সব সময় কমেন্ট করে // Arrange, // Act, // Assert ব্লক আলাদা করে লিখবেন (আমি ওপরের কোডে দেখিয়েছি)। এটি প্রফেশনাল লেভেলে কোড রিডেবিলিটি বহুগুণ বাড়িয়ে দেয়।

4. DTO Initialization (C# 12+ Feature): যখন আপনি টেস্ট ডেটা (Arrange) তৈরি করেন, তখন C# এর নতুন Target-typed new এক্সপ্রেশন ব্যবহার করে কোড আরও ছোট করতে পারেন:

// PersonAddRequest লেখার দরকার নেই, শুধু new() দিলেই হবে
PersonAddRequest personAddRequest = new()
{
    PersonName = "Rahim",
    // ...
};