আমি আপনার এক্সপার্ট সফটওয়্যার ইঞ্জিনিয়ারিং ট্রেইনার। কোর্স আউটলাইন অনুযায়ী, আমরা এখন Section 15: xUnit testing-এর ভেতরে আছি। এই লেকচারটিতে আমরা TDD (Test Driven Development) অ্যাপ্রোচ ফলো করে UpdatePerson মেথডের জন্য Unit Test লেখা শিখেছি। এর ইমপ্লিমেন্টেশন আমরা আগামী লেকচারে করব।
চলুন, আজকের লেকচারটি গুছিয়ে ও বিস্তারিতভাবে বুঝে নেওয়া যাক।
📝 Lecture Summary (Quick Revision)
ভবিষ্যতে দ্রুত রিভিশন দেওয়ার জন্য সম্পূর্ণ লেকচারের মূল কাজগুলো নিচে লিস্ট আকারে দেওয়া হলো:
- Interface Setup:
IPersonsServiceইন্টারফেসেUpdatePersonনামের একটি নতুন মেথড ডিক্লেয়ার করা, যাPersonUpdateRequestরিসিভ করবে এবংPersonResponseরিটার্ন করবে। - Dummy Implementation:
PersonsServiceক্লাসে মেথডটির ইনিশিয়াল বা ডামি ইমপ্লিমেন্টেশন করা (যেখানেNotImplementedExceptionথ্রো করা হয়)। - Test Case 1 (Null Request): ইনপুট হিসেবে
nullপাস করলে মেথডটি যেনArgumentNullExceptionথ্রো করে তা টেস্ট করা। - Test Case 2 (Invalid Person ID): একটি ফেক বা নতুন জেনারেট করা
PersonIDদিয়ে আপডেট করতে চাইলে যেনArgumentExceptionথ্রো করে তা টেস্ট করা। - Test Case 3 (Validation Error - Null Name): সঠিক
PersonIDথাকার পরও যদি আপডেটের সময়PersonNamenullকরে দেওয়া হয়, তবে যেন ভ্যালিডেশন এরর হিসেবেArgumentExceptionথ্রো করে তা টেস্ট করা। - Test Case 4 (Successful Update): সঠিকভাবে ডেটা আপডেট করলে তা আসলেই ডাটাবেসে সেভ হচ্ছে কিনা, তা
GetPersonByPersonIdদিয়ে পুনরায় তুলে এনেAssert.Equalএর মাধ্যমে যাচাই করা। - TDD Verification: টেস্টগুলো রান করে দেখা যে সবগুলো ফেইল করছে, কারণ মেথডটিতে এখনো আসল লজিক লেখা হয়নি।
🧠 Comprehensive Breakdown
এই লেকচারে আমরা আপডেট অপারেশনের ৪টি ভিন্ন ভিন্ন সিনারিও টেস্ট করেছি। চলুন প্রতিটি ধাপ বিস্তারিতভাবে বুঝে নিই।
১. Interface-এ Update Method ডিক্লেয়ারেশন (Priority: 7/10)
Concept & Why: যখন Controller থেকে ইউজার কোনো বিদ্যমান Person-এর ডেটা আপডেট করার রিকোয়েস্ট পাঠাবে, তখন এই মেথডটি ব্যবহার করা হবে। এটি ইনপুট হিসেবে DTO (PersonUpdateRequest) রিসিভ করে এবং আপডেট হওয়ার পর সেই ডেটাকেই PersonResponse হিসেবে রিটার্ন করে।
// ServiceContracts/IPersonsService.cs
/// <summary>
/// Updates the specified person details based on the given person ID
/// </summary>
/// <param name="personUpdateRequest">Person details to update, including person id</param>
/// <returns>Returns the person response object after updation</returns>
PersonResponse UpdatePerson(PersonUpdateRequest? personUpdateRequest);
২. Unit Test 1: Null Request Check (Priority: 9/10)
Concept & Why: ইউজার যদি ভুল করে PersonUpdateRequest এর বদলে সম্পূর্ণ null পাঠায়, তবে সিস্টেম ক্র্যাশ না করে যেন ArgumentNullException থ্রো করে, সেটি টেস্ট করা।
#region UpdatePerson
[Fact]
public void UpdatePerson_NullPerson()
{
// Arrange
PersonUpdateRequest? person_update_request = null;
// Act & Assert
Assert.Throws<ArgumentNullException>(() =>
{
_personsService.UpdatePerson(person_update_request);
});
}
৩. Unit Test 2: Invalid Person ID Check (Priority: 10/10)
Concept & Why: আপডেটের সবচেয়ে বড় শর্ত হলো, যে ইউজারকে আপডেট করতে চাই, তাকে আগে সিস্টেমে থাকতে হবে। যদি আমরা এমন কোনো PersonID পাঠাই যা ডাটাবেসে নেই (যেমন এখানে আমরা Guid.NewGuid() দিয়ে একটি ফেক ID বানিয়েছি), তবে সিস্টেমের উচিত ArgumentException থ্রো করা।
[Fact]
public void UpdatePerson_InvalidPersonID()
{
// Arrange: সম্পূর্ণ নতুন একটি ID জেনারেট করা হচ্ছে যা সিস্টেমে নেই
PersonUpdateRequest? person_update_request = new PersonUpdateRequest() { PersonID = Guid.NewGuid() };
// Act & Assert
Assert.Throws<ArgumentException>(() =>
{
_personsService.UpdatePerson(person_update_request);
});
}
৪. Unit Test 3: Null PersonName (Validation Check) (Priority: 10/10)
Concept & Why: আমরা চেক করতে চাই যে, আপডেটের সময়ও আমাদের Data Annotations (যেমন [Required]) ঠিকমতো কাজ করছে কিনা।
এর জন্য আমরা প্রথমে একটি ভ্যালিড Person অ্যাড করব, তারপর তার ডেটাকে ToPersonUpdateRequest() দিয়ে আপডেট রিকোয়েস্টে কনভার্ট করব। এরপর ইচ্ছাকৃতভাবে PersonName কে null করে দিয়ে আপডেট মেথড কল করব। এটি ArgumentException থ্রো করবে।
[Fact]
public void UpdatePerson_PersonNameIsNull()
{
// Arrange: Country এবং Person তৈরি করা (কোড সংক্ষেপিত)
/* ... add country and add person logic ... */
// AddPerson থেকে পাওয়া রেসপন্সকে UpdateRequest এ কনভার্ট করা
PersonUpdateRequest person_update_request = person_response_from_add.ToPersonUpdateRequest();
// ইচ্ছাকৃতভাবে নাম null করে দেওয়া হচ্ছে (Validation fail করার জন্য)
person_update_request.PersonName = null;
// Act & Assert
Assert.Throws<ArgumentException>(() =>
{
_personsService.UpdatePerson(person_update_request);
});
}
৫. Unit Test 4: Proper Person Details (Successful Update) (Priority: 10/10)
Concept & Why: এটি পজিটিভ টেস্ট। আমরা একটি ভ্যালিড Person-এর নাম এবং ইমেইল চেঞ্জ করে আপডেট করব।
আপডেট করার পর আমরা GetPersonByPersonId কল করে ডাটাবেস থেকে আবার ডেটা তুলে আনব। এরপর আমরা যাচাই করব যে, আপডেট করার পর যে রেসপন্স পেয়েছিলাম (Actual) এবং নতুন করে ডাটাবেস থেকে যা তুলে আনলাম (Expected), তারা হুবহু এক কিনা।
[Fact]
public void UpdatePerson_PersonFullDetailsUpdation()
{
// Arrange: Country এবং Person তৈরি করা (কোড সংক্ষেপিত)
/* ... add country and add person logic ... */
// কনভার্ট করে নতুন নাম ও ইমেইল সেট করা
PersonUpdateRequest person_update_request = person_response_from_add.ToPersonUpdateRequest();
person_update_request.PersonName = "William";
person_update_request.Email = "william@example.com";
// Act 1: আপডেট মেথড কল করা
PersonResponse person_response_from_update = _personsService.UpdatePerson(person_update_request);
// Act 2: ডাটাবেস থেকে পুনরায় ডেটা তুলে আনা
PersonResponse? person_response_from_get = _personsService.GetPersonByPersonId(person_update_request.PersonID);
// Assert: চেক করা যে ডাটাবেসের ডেটা এবং আপডেটের ডেটা সেম কিনা
Assert.Equal(person_response_from_get, person_response_from_update);
}
#endregion
🚀 Modern C# (.NET 10 Update) & Smarter Approach
লেকচারের পদ্ধতিটি লজিক্যালি শতভাগ সঠিক। তবে টেস্ট ডেটা তৈরি (Arrange) করার কোডগুলো অনেক বড় হয়ে যাচ্ছে এবং বারবার রিপিট হচ্ছে।
Avoid Code Duplication (Test Data Builders):
রিয়েল-ওয়ার্ল্ড প্রজেক্টে আমরা Test Data Builder Pattern বা AutoFixture এর মতো লাইব্রেরি ব্যবহার করি। এতে Person অ্যাড করার এত লম্বা কোড বারবার লিখতে হয় না।
Modern Refactoring Example (Using a Helper Method):
// Helper Method in Test Class
private async Task<PersonResponse> CreateAndAddDummyPersonAsync()
{
var country = _countriesService.AddCountry(new CountryAddRequest { CountryName = "UK" });
return _personsService.AddPerson(new PersonAddRequest
{
PersonName = "John",
CountryID = country.CountryID
// ...
});
}
// Then in your Test Method:
[Fact]
public async Task UpdatePerson_PersonFullDetailsUpdation()
{
// Arrange (Now just a few lines!)
var existingPerson = await CreateAndAddDummyPersonAsync();
var updateRequest = existingPerson.ToPersonUpdateRequest() with
{
PersonName = "William",
Email = "william@example.com"
}; // using C# record 'with' expression makes it cleaner
// Act & Assert
// ...
}
🏆 Best Practices (For Update Testing)
- Verify Database State: Update মেথড টেস্ট করার সময় শুধু মেথডের রিটার্ন ভ্যালু চেক করাই যথেষ্ট নয়। লেকচারার যেমন
GetPersonByPersonIdদিয়ে ডাটাবেস থেকে আবার ডেটা তুলে এনে চেক করেছেন, এটিই বেস্ট প্র্যাকটিস। এর মাধ্যমে নিশ্চিত হওয়া যায় যে ডেটা শুধু মেমরিতে নয়, আসল স্টোরেজেও আপডেট হয়েছে। - Test Isolation:
UpdatePersonমেথডের প্রতিটি টেস্টে নতুন করে Country এবং Person তৈরি করা হয়েছে। এটি Test Isolation এর দারুণ উদাহরণ। কখনোই এক টেস্টের ডেটার ওপর ভিত্তি করে অন্য টেস্ট লেখা উচিত নয়। - Validation Focus: যখন আপনি Validation টেস্ট করবেন (যেমন
PersonNameIsNull), তখন খেয়াল রাখবেন যেন বাকি সবগুলো ফিল্ড ভ্যালিড থাকে। শুধু যে ফিল্ডটি টেস্ট করছেন, সেটিই ইনভ্যালিড করবেন। নতুবা অন্য কোনো কারণে টেস্ট ফেইল করলে আপনি বুঝতে পারবেন না আসল কারণ কী।