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

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

📝 Lecture Summary (Quick Revision)

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

  • Interface Setup: IPersonsService ইন্টারফেসে GetFilteredPersons নামের একটি নতুন মেথড ডিক্লেয়ার করা, যা searchBy (প্রপার্টির নাম) এবং searchString (সার্চ টেক্সট) রিসিভ করবে।
  • Dummy Implementation: PersonsService ক্লাসে মেথডটির ইনিশিয়াল বা ডামি ইমপ্লিমেন্টেশন করা (যেখানে NotImplementedException থ্রো করা হয়)।
  • Test Case 1 (Empty Search Text): GetFilteredPersons_EmptySearchText মেথড তৈরি করে চেক করা যে, সার্চ স্ট্রিং হিসেবে "" (Empty string) দিলে এটি সবগুলো Person রিটার্ন করে কিনা।
  • Test Case 2 (Search By Name): GetFilteredPersons_SearchByPersonName মেথড তৈরি করে চেক করা যে, স্পেসিফিক টেক্সট (যেমন: "ma") দিয়ে সার্চ করলে Case-Insensitive (বড়/ছোট হাতের অক্ষর ইগনোর করে) ভাবে শুধু ম্যাচিং ডেটাগুলোই রিটার্ন আসে কিনা।
  • TDD Verification: টেস্টগুলো রান করে দেখা যে ফেইল করছে, কারণ মেথডটিতে এখনো আসল লজিক লেখা হয়নি। একইসাথে ITestOutputHelper এর মাধ্যমে Expected ভ্যালুগুলো প্রিন্ট হয়ে আসা চেক করা।

🧠 Comprehensive Breakdown

এই লেকচারে আমরা মূলত Search অপারেশনের দুটি ভিন্ন সিনারিও টেস্ট করেছি। চলুন প্রতিটি ধাপ বিস্তারিতভাবে বুঝে নিই।

১. Interface-এ Search Method ডিক্লেয়ারেশন (Priority: 7/10)

Concept & Why: যখন Controller থেকে ইউজার কোনো স্পেসিফিক ফিল্ড (যেমন: Name, Email) ধরে সার্চ করতে চাইবে, তখন এই মেথডটি ব্যবহার করা হবে। এখানে searchBy হলো কোন প্রপার্টি ধরে সার্চ হবে তা, আর searchString হলো ইউজার কী লিখে সার্চ করেছে তা।

// ServiceContracts/IPersonsService.cs
/// <summary>
/// Returns all persons that match with the given search field and search string
/// </summary>
/// <param name="searchBy">Search field to search</param>
/// <param name="searchString">Search string to search</param>
/// <returns>Returns all matching persons</returns>
List<PersonResponse> GetFilteredPersons(string searchBy, string? searchString);
 

২. Unit Test 1: Empty Search Text (Priority: 9/10)

Concept & Why: একটি স্ট্যান্ডার্ড সার্চ ফিচারের নিয়ম হলো, ইউজার যদি সার্চ বক্সে কিছুই না লিখে এন্টার দেয় (Empty string), তবে ডাটাবেসে থাকা সবগুলো ডেটাই তার দেখা উচিত (ঠিক যেন GetAllPersons এর মতো)।

#region GetFilteredPersons
 
[Fact]
public void GetFilteredPersons_EmptySearchText()
{
    // Arrange: আগের টেস্টের মতোই Country এবং ৩ জন Person অ্যাড করা হলো (কোড সংক্ষেপিত)
    /* ... 3 persons added ... */
 
    // Act: Search করা হচ্ছে, কিন্তু searchString ফাঁকা ("") দেওয়া হয়েছে
    List<PersonResponse> persons_list_from_search = _personsService.GetFilteredPersons(nameof(Person.PersonName), "");
 
    // Assert: চেক করা হচ্ছে ইনসার্ট করা ৩ জন Person-ই রেজাল্টে আছে কিনা
    foreach (PersonResponse person_response_from_add in person_response_list_from_add)
    {
        Assert.Contains(person_response_from_add, persons_list_from_search);
    }
}
 

(নোট: হার্ডকোডেড স্ট্রিং "PersonName" লেখার চেয়ে nameof(Person.PersonName) ব্যবহার করা বেটার, কারণ পরে ক্লাসে প্রপার্টির নাম চেঞ্জ করলে এখানে অটোমেটিক আপডেট হয়ে যাবে)।

৩. Unit Test 2: Proper Search Text (Case-Insensitive) (Priority: 10/10)

Concept & Why: এটি হলো আসল সার্চের টেস্ট। আমরা যদি "ma" লিখে সার্চ করি, তবে Mary এবং Rahman দুজনের নামই রেজাল্টে আসা উচিত, কারণ দুজনের নামেই "ma" আছে। এখানে Case-Insensitive (বড় হাতের ‘M’ নাকি ছোট হাতের ‘m’) সার্চ হওয়াটা অত্যন্ত জরুরি।

[Fact]
public void GetFilteredPersons_SearchByPersonName()
{
    // Arrange: আগের মতোই ৩ জন Person অ্যাড করা হলো (কোড সংক্ষেপিত)
    /* ... 3 persons added ... */
 
    // Act: "ma" দিয়ে Search করা হচ্ছে
    List<PersonResponse> persons_list_from_search = _personsService.GetFilteredPersons(nameof(Person.PersonName), "ma");
 
    // Assert: যাচাই করা হচ্ছে
    foreach (PersonResponse person_response_from_add in person_response_list_from_add)
    {
        if (person_response_from_add.PersonName != null)
        {
            // Case-insensitive চেক: যদি ইনসার্ট করা নামের ভেতরে "ma" থাকে
            if (person_response_from_add.PersonName.Contains("ma", StringComparison.OrdinalIgnoreCase))
            {
                // তাহলে আশা করছি ওই Person টি সার্চের রেজাল্টেও থাকবে
                Assert.Contains(person_response_from_add, persons_list_from_search);
            }
        }
    }
}
#endregion
 

৪. TDD Verification & Debugging Output (Priority: 5/10)

Concept: টেস্টগুলো রান করলে সেগুলো ফেইল করে (কারণ মেথডটি এখনো ইমপ্লিমেন্ট করা হয়নি)। কিন্তু মজার ব্যাপার হলো, টেস্ট ফেইল করলেও Test Explorer-এর “Standard Output” বা Details প্যানেলে আমরা Expected Person-গুলোর ডিটেইলস দেখতে পাই। Why: কারণ আগের লেকচারে আমরা _testOutputHelper.WriteLine() ব্যবহার করে Expected লিস্ট প্রিন্ট করার লজিক লিখেছিলাম। এটি ডিবাগিংয়ের সময় অনেক কাজে দেয়।


🚀 Modern C# (.NET 10 Update) & Smarter Approach

লেকচারের পদ্ধতিটি লজিক্যালি শতভাগ সঠিক। তবে টেস্ট ডেটা তৈরি (Arrange) করার কোডগুলো অনেক বড় হয়ে যাচ্ছে।

1. Avoid Code Duplication (Helper Methods for Setup): লেকচারার নিজেই বলেছেন, AddPerson এর কাজগুলো বারবার কপি-পেস্ট করাটা বিরক্তিকর। রিয়েল-ওয়ার্ল্ড প্রজেক্টে আমরা টেস্ট ক্লাসের ভেতরে একটি প্রাইভেট মেথড বানিয়ে নিই ডেটা সেটআপ করার জন্য।

Modern Refactoring Example:

// Helper Method in Test Class
private List<PersonResponse> CreateAndAddDummyPersons()
{
    // (দেশের ডেটা তৈরি এবং Person অ্যাড করার সব লজিক এখানে থাকবে)
    // রিটার্ন করবে লিস্ট অফ PersonResponse
}
 
// Then in your Test Method:
[Fact]
public void GetFilteredPersons_SearchByPersonName()
{
    // Arrange (Now just one line!)
    var expectedPersons = CreateAndAddDummyPersons();
 
    // Act
    var actualPersons = _personsService.GetFilteredPersons(nameof(Person.PersonName), "ma");
 
    // Assert
    // ...
}
 

2. Fluent Assertions: xUnit এর বিল্ট-ইন Assert-এর চেয়ে FluentAssertions লাইব্রেরি ব্যবহার করাটা আধুনিক C#-এ বেশ জনপ্রিয়। এটি কোডকে ইংরেজির মতো রিডেবল করে তোলে।

// Using FluentAssertions library (Just an example of modern standard)
actualPersons.Should().Contain(p => p.PersonName.Contains("ma", StringComparison.OrdinalIgnoreCase));
 

🏆 Best Practices (For Searching and Testing)

  1. Use nameof() Operator: স্ট্রিং হিসেবে প্রপার্টির নাম (যেমন "Email") পাস না করে সব সময় nameof(Person.Email) ব্যবহার করবেন। এতে টাইপো (Typo) হওয়ার সম্ভাবনা জিরো হয়ে যায়।
  2. Case-Insensitive Search: ডাটাবেস বা মেমরি থেকে টেক্সট সার্চ করার সময় সব সময় StringComparison.OrdinalIgnoreCase ব্যবহার করা উচিত, নতুবা ক্যাপিটাল/স্মল লেটারের পার্থক্যের কারণে ইউজার ডেটা খুঁজে পাবে না।
  3. DRY in Tests: যদিও টেস্টিং কোডে কিছুটা রিপিটেশন মেনে নেওয়া হয়, তবুও দীর্ঘ সেটআপ (Arrange) ব্লকগুলো একটি আলাদা প্রাইভেট মেথডে সরিয়ে নেওয়াই বেস্ট প্র্যাকটিস।