DCSIMG
Harnessing the Power of AppDomains in UnitTests - Liran Chen's Blog

Liran Chen's Blog

.Net Internals, Debugging, Multithreading - and More!

Harnessing the Power of AppDomains in UnitTests

אחת הבעיות שיכולות להתעורר במהלך בדיקות UnitTests היא שינוי מצב ה-State בין הבדיקות.
למה הכוונה? יתכנו מצבים בהם הרצה של Test אחד יכולה להשפיע על מהלך הבדיקה של Test אחר. אם במידה וקיים משתנה סטטי כלשהו באפליקציה שלכם, וב-Test הראשון איתחלתם את ערכו ל-10 למשל, אזי ברגע שתפעיל את ה-Test השני, הערך של אותו משתנה סטאטי עדיין יהיה 10. ובמידה והבדיקה תערב בצורה כלשהיא את אותו משתנה סטאטי, יתכן ולוגיקת הבדיקה תפגע.

איך ניתן להמנע ממצב כזה?
אם ניקח לדוגמה את nUnit, אנחנו יכולים להשתמש ב-Attribute'ים המתאימים בשביל ליצור פונקציות שיבוצעו לפני/אחרי כל קריאה ל-Test.
לכן, הדרך "המהירה ביותר" היא פשוט לכתוב פונקציה כזאת שתאתחל את כל המשתנים הסטאטים בתוכנית. אבל מן הסתם, זהו פתרון לא מומלץ, ולא נכון.
הוא לא מומלץ בגלל שבכל פעם שתוסיפו משתנה סטאטי חדש לתוכנית שלכם, תצטרכו לעדכן את ה-UnitTests ... משימה כמעט בלתי אפשרית.
ועוד יותר בעייתי מזה: זה לא יעבוד. כלומר, זה יעבוד ... אבל לא תמיד. קחו למשל מצב בו יש לך מחלקה עם בנאי סטאטי כלשהו. אותו בנאי סטאטי יקרא אך ורק פעם אחת, והפעם הזאת תהיה ב-Test הראשון. מהנקודה הזאת והלאה, לא יהיה ניתן להריץ מחדש את אותו בנאי סטאטי במהלך החיים של ה-AppDomain הנוכחי.

הפתרון המתאים למצב הזה, יהיה ליצור AppDomain נפרד שישמש אותנו להרצת הבדיקות.
לפני כל הרצה של Test ניצור אותו מחדש, וכך למעשה נשיג "תנאי מעבדה" נקיים, ללא כל עקבות של Test'ים אחרים שהרצנו בעבר. בצורה כזאת, שום Test לא יצור איזשהו State שישאר גם ל-Test'ים הבאים.

ביצוע קוד על AppDomain נפרד

הדבר הראשון שנצטרך לדעת לעשות, זה ליצור AppDomain חדש, ולגרום לקוד שלנו להתבצע עליו.
הנה הדוגמה:

// your class should implement MarshalByRefObject for

// allowing it to be accessed via a different AppDomain

public class Program : MarshalByRefObject

{

public void PrintAppDomainName()

{

Console.WriteLine(AppDomain.CurrentDomain.FriendlyName);

}

static void Main()

{

Assembly entryAssembly = Assembly.GetEntryAssembly();

// create the new AppDomain

AppDomain myDomain = AppDomain.CreateDomain("MyDomain");

// create an instance on the newly created AppDomain

Program remoteObject = (Program)myDomain.CreateInstanceAndUnwrap

(entryAssembly.FullName, typeof(Program).FullName);

remoteObject.PrintAppDomainName();

Program localObject = new Program();

localObject.PrintAppDomainName();

// remember to unload the AppDomain when finished

AppDomain.Unload(myDomain);

// Output:

// MyDomain

// ConsoleApplication2.vshost.exe

}

}


נשים לב למספר אלמנטים עיקריים בקוד:
  • Program : MarshalByRefObject
    כברירת מחדל, מרחב הגישה הרחב ביותר עבור מופעים של אובייקטים, הוא ה-AppDomain. כלומר, אם ב-Process שלי קיימים מספר AppDomain'ים נפרדים, כל AppDomain יכול לגשת רק לאובייקטים שנמצאים בתוך אותו AppDomain.
    על מנת לאפשר גישה לאובייקט שנמצא מחוץ לגבולות ה-AppDomain, נצטרך שהמחלקה תירש מ-MarshalByRefObject. בלי לפרט בדיוק מה ואיך היא עושה, נסכם שבמקרה שלנו היא נותנת למחלקה שלנו את הפונקציונליות שתאפשר לנו לגשת למופעים שלה בין AppDomain'ים שונים.

  • AppDomain myDomain = AppDomain.CreateDomain("MyDomain")
    די מובן מאליו. כאן אנחנו יוצרים AppDomain חדש עם השם "MyDomain". לאחר ביצוע השורה הזאת, יהיו לנו ב-Process שני AppDomain'ים נפרדים. הראשון הוא ה"ברירת מחדל", אותו AppDomain שנטען לנו אוטומטית ברגע שהפעלנו את התוכנית, והשני הוא אותו "MyDomain" שיצרנו עכשיו.

  • Program remoteObject = (Program)myDomain.CreateInstanceAndUnwrap

    (entryAssembly.FullName, typeof(Program).FullName)

    הנה החלק המעניין האמיתי. כאן אנחנו למעשה פונים ל-AppDomain החדש שלנו, ואומרים לו ליצור מופע של הטיפוס Program. המופע החדש שנוצר נמצא מחוץ לגבולות ה-AppDomain שעליו אנו רצים כרגע, לכן מדובר למעשה ב"מופע מרוחק".
    מהרגע שיצרנו אותו, כל פונקציה שנפעילו עליו, תתבצע תחת ה-Context של ה-AppDomain החדש.


  • AppDomain.Unload(myDomain)
    לאחר שסיימנו לעבוד עם MyDomain, אפשר לעשות לו Unload. מה שלמעשה ישחרר את המשאבים בהם הוא עשה שימוש. לאחר הנקודה הזאת, לא יהיה ניתן להשתמש כבר ב-remoteObject. ולמעשה כל ניסיון פניה אליו תגרום ליציאה של התוכנית.
AppDomain חדש, ולגרום לקוד שלנו להתבצע עליו.
מה שנשאר עכשיו, זה ליישם את הרעיון הזה ב-UnitTests שלנו.
בשביל לעשות את זה, נצטרך "להזריק קוד" לתוך ה-AppDomain הנפרד. בשביל לעשות את זה, ניצור מופע של מחלקת הבדיקות שלנו על ה-AppDomain הנפרד, נחלץ מופע של MethodInfo עבור הפונקציה שאנחנו רוצים להריץ, ונשתמש בפונקציית ה-Invoke שלה בשביל לגרום לה להתבצע בתוך הקונטקסט של המופע המרוחק שלנו.

התוצאה הסופית יכולה להתתבסס על הקוד הבא:

public class MyUnitTests : MarshalByRefObject

{

private const string TESTING_APPDOMAIN_NAME = "Testing AppDomain";

private static AppDomain m_testingAppDomain;

private static MyUnitTests m_remoteTestBed;

public void MyTest()

{

if (AppDomain.CurrentDomain.FriendlyName != TESTING_APPDOMAIN_NAME)

{

MethodBase curMethodInfo = MethodInfo.GetCurrentMethod();

curMethodInfo.Invoke(m_remoteTestBed, null);

return;

}

Console.WriteLine("Running on AppDomain: {0}", AppDomain.CurrentDomain.FriendlyName);

}

static void Main()

{

MyUnitTests local = new MyUnitTests();

local.SetUp();

// note that we are calling the method on the main AppDomain, just like

// what would happen if we call it from nUnit.

local.MyTest();

local.Teardown();

}

public void SetUp()

{

Assembly entryAssembly = Assembly.GetEntryAssembly();

m_testingAppDomain = AppDomain.CreateDomain(TESTING_APPDOMAIN_NAME);

m_remoteTestBed = (MyUnitTests)m_testingAppDomain.CreateInstanceAndUnwrap

(entryAssembly.FullName, typeof(MyUnitTests).FullName);

}

public void Teardown()

{

AppDomain.Unload(m_testingAppDomain);

m_testingAppDomain = null;

}

}

שלח תגובה

(שדה חובה)  

(שדה חובה)  

(אופציונלי)

(שדה חובה) 

Please add 7 and 2 and type the answer here:


Enter the numbers above: