การทดสอบหน่วยบุคคลด้วย Entity Framework 6 เป็นอย่างไรคุณควรใส่ใจ


170

ฉันเพิ่งเริ่มด้วยการทดสอบหน่วยและ TDD โดยทั่วไป ฉันเคยขยิบตามาก่อน แต่ตอนนี้ฉันมุ่งมั่นที่จะเพิ่มลงในเวิร์กโฟลว์ของฉันและเขียนซอฟต์แวร์ที่ดีขึ้น

ฉันถามคำถามเมื่อวานว่าแบบนี้รวมอยู่ด้วย แต่ดูเหมือนจะเป็นคำถามของมันเอง ฉันนั่งลงเพื่อเริ่มใช้งานคลาสบริการที่ฉันจะใช้เพื่อสรุปตรรกะทางธุรกิจจากคอนโทรลเลอร์และแผนที่ไปยังโมเดลเฉพาะและการโต้ตอบของข้อมูลโดยใช้ EF6

ปัญหาคือฉันมีสิ่งกีดขวางบนถนนแล้วเพราะฉันไม่ต้องการแยก EF ออกจากที่เก็บ (มันจะยังคงให้บริการนอกบริการสำหรับข้อความค้นหาเฉพาะ ฯลฯ ) และต้องการทดสอบบริการของฉัน (จะใช้บริบทของ EF) .

ที่นี่ฉันเดาเป็นคำถามมีจุดทำเช่นนี้หรือไม่ ถ้าเป็นเช่นนั้นผู้คนกำลังทำอะไรอยู่ในป่าท่ามกลางแสงของรอยรั่วที่เกิดจาก IQueryable และการโพสต์ที่ยอดเยี่ยมมากมายของLadislav Mrnkaในเรื่องของการทดสอบหน่วยไม่ตรงไปตรงมาเพราะความแตกต่างในผู้ให้บริการ Linq เมื่อทำงานกับหน่วยความจำ การนำไปใช้งานตามที่ระบุในฐานข้อมูลเฉพาะ

รหัสที่ฉันต้องการทดสอบดูเหมือนง่ายมาก (นี่เป็นเพียงรหัสจำลองเพื่อลองและเข้าใจสิ่งที่ฉันกำลังทำอยู่ฉันต้องการผลักดันการสร้างโดยใช้ TDD)

บริบท

public interface IContext
{
    IDbSet<Product> Products { get; set; }
    IDbSet<Category> Categories { get; set; }
    int SaveChanges();
}

public class DataContext : DbContext, IContext
{
    public IDbSet<Product> Products { get; set; }
    public IDbSet<Category> Categories { get; set; }

    public DataContext(string connectionString)
                : base(connectionString)
    {

    }
}

บริการ

public class ProductService : IProductService
{
    private IContext _context;

    public ProductService(IContext dbContext)
    {
        _context = dbContext;
    }

    public IEnumerable<Product> GetAll()
    {
        var query = from p in _context.Products
                    select p;

        return query;
    }
}

ขณะนี้ฉันอยู่ในความคิดของการทำบางสิ่ง:

  1. การเยาะเย้ยบริบทของ EF ด้วยวิธีนี้การเยาะเย้ย EF เมื่อการทดสอบหน่วยหรือใช้กรอบการเยาะเย้ยโดยตรงบนอินเตอร์เฟสเช่นขั้นต่ำการรับความเจ็บปวดที่การทดสอบหน่วยอาจผ่าน แต่ไม่จำเป็นต้องจบการทำงาน
  2. อาจจะใช้ความพยายามในการเยาะเย้ย EF - ฉันไม่เคยใช้มันและไม่แน่ใจว่ามีคนอื่นกำลังใช้งานอยู่ในป่าหรือไม่?
  3. ไม่รำคาญที่จะทดสอบสิ่งใด ๆ ที่เพียงแค่โทรกลับไปที่ EF - ดังนั้นวิธีการบริการที่สำคัญที่เรียกว่า EF โดยตรง (getAll ฯลฯ ) ไม่ใช่การทดสอบหน่วย

ทุกคนที่อยู่ที่นั่นทำสิ่งนี้โดยไม่มี Repo และประสบความสำเร็จหรือไม่


เฮ้ Modika ฉันกำลังคิดเกี่ยวกับเรื่องนี้เมื่อเร็ว ๆ นี้ (เพราะคำถามนี้: stackoverflow.com/questions/25977388/ ...... ) ในนั้นฉันพยายามอธิบายอีกเล็กน้อยอย่างเป็นทางการว่าฉันทำงานอย่างไรในขณะนี้ แต่ฉันชอบที่จะได้ยินว่า คุณกำลังทำมัน
samy

สวัสดี @ ซามี่วิธีที่เราตัดสินใจทำไม่ใช่การทดสอบหน่วยใด ๆ ที่สัมผัสกับ EF โดยตรง แบบสอบถามได้รับการทดสอบ แต่เป็นการทดสอบการรวมระบบไม่ใช่การทดสอบหน่วย การเยาะเย้ย EF รู้สึกสกปรกเล็กน้อย แต่โครงการนี้มีขนาดเล็กมากดังนั้นผลกระทบด้านประสิทธิภาพของการมีการทดสอบจำนวนมากที่กระทบฐานข้อมูลนั้นไม่ได้เป็นข้อกังวลอย่างแท้จริงดังนั้นเราจึงสามารถนำไปปฏิบัติได้จริง ฉันยังไม่แน่ใจ 100% ว่าวิธีที่ดีที่สุดที่จะซื่อสัตย์กับคุณอย่างสมบูรณ์คือในบางจุดที่คุณกำลังจะไปถึง EF (และฐานข้อมูลของคุณ) และการทดสอบหน่วยไม่ตรงกับฉันเลย
Modika

คำตอบ:


186

นี่เป็นหัวข้อที่ฉันสนใจมากมีนักสอนหลายคนที่บอกว่าคุณไม่ควรทดสอบเทคโนโลยีเช่น EF และ NHibernate พวกเขาถูกต้องพวกเขาถูกทดสอบอย่างเข้มงวดแล้วและตามคำตอบก่อนหน้าระบุว่ามักจะไม่มีประโยชน์ที่จะใช้เวลาทดสอบมากมายในสิ่งที่คุณไม่ได้เป็นเจ้าของ

อย่างไรก็ตามคุณมีฐานข้อมูลอยู่ด้านล่าง! นี่เป็นวิธีที่ความคิดของฉันแตกสลายคุณไม่จำเป็นต้องทดสอบว่า EF / NH ทำงานอย่างถูกต้อง คุณต้องทดสอบว่าการแม็พ / การนำไปใช้งานทำงานกับฐานข้อมูลของคุณ ในความคิดของฉันนี่เป็นหนึ่งในส่วนที่สำคัญที่สุดของระบบที่คุณสามารถทดสอบได้

พูดอย่างเคร่งครัด แต่เรากำลังย้ายออกจากโดเมนของการทดสอบหน่วยและเป็นการทดสอบการรวม แต่หลักการยังคงเหมือนเดิม

สิ่งแรกที่คุณต้องทำคือการเยาะเย้ย DAL ของคุณเพื่อให้ BLL ของคุณสามารถทดสอบได้โดยอิสระจาก EF และ SQL นี่คือการทดสอบหน่วยของคุณ ต่อไปคุณต้องออกแบบการทดสอบการรวมระบบเพื่อพิสูจน์ DAL ของคุณในความคิดของฉันสิ่งเหล่านี้มีความสำคัญทุกประการ

มีสองสิ่งที่ควรพิจารณา:

  1. ฐานข้อมูลของคุณต้องอยู่ในสถานะที่ทราบพร้อมการทดสอบแต่ละครั้ง ระบบส่วนใหญ่ใช้การสำรองข้อมูลหรือสร้างสคริปต์สำหรับสิ่งนี้
  2. การทดสอบแต่ละครั้งจะต้องทำซ้ำได้
  3. การทดสอบแต่ละครั้งจะต้องเป็นอะตอม

มีสองวิธีหลักในการตั้งค่าฐานข้อมูลของคุณวิธีแรกคือการเรียกใช้สคริปต์สร้าง UnitTest DB สิ่งนี้ช่วยให้มั่นใจได้ว่าฐานข้อมูลการทดสอบหน่วยของคุณจะอยู่ในสถานะเดิมเมื่อเริ่มต้นการทดสอบแต่ละครั้ง (คุณอาจรีเซ็ตหรือรันการทดสอบแต่ละครั้งในธุรกรรมเพื่อให้แน่ใจว่านี่)

ตัวเลือกอื่นของคุณคือสิ่งที่ฉันทำเรียกใช้การตั้งค่าเฉพาะสำหรับการทดสอบแต่ละรายการ ฉันเชื่อว่านี่เป็นวิธีที่ดีที่สุดสำหรับสองเหตุผลหลัก:

  • ฐานข้อมูลของคุณง่ายขึ้นคุณไม่จำเป็นต้องมีสคีมาทั้งหมดสำหรับการทดสอบแต่ละครั้ง
  • การทดสอบแต่ละครั้งมีความปลอดภัยมากขึ้นหากคุณเปลี่ยนค่าหนึ่งค่าในสคริปต์สร้างของคุณการทดสอบนั้นจะไม่ทำให้การทดสอบอื่น ๆ

น่าเสียดายที่คุณประนีประนอมที่นี่ความเร็ว ต้องใช้เวลาในการเรียกใช้การทดสอบเหล่านี้เพื่อเรียกใช้สคริปต์การตั้งค่า / การฉีกขาดเหล่านี้ทั้งหมด

ประเด็นสุดท้ายข้อหนึ่งอาจเป็นเรื่องยากมากที่จะเขียน SQL จำนวนมากเพื่อทดสอบ ORM ของคุณ นี่คือที่ฉันใช้วิธีที่น่ารังเกียจมาก (ครูสอนที่นี่จะไม่เห็นด้วยกับฉัน) ฉันใช้ ORM ของฉันเพื่อสร้างแบบทดสอบของฉัน! แทนที่จะมีสคริปต์แยกต่างหากสำหรับการทดสอบ DAL ทุกครั้งในระบบของฉันฉันมีขั้นตอนการตั้งค่าการทดสอบที่สร้างวัตถุแนบไปกับบริบทและบันทึกไว้ จากนั้นฉันก็ทำการทดสอบ

นี่ยังห่างไกลจากทางออกที่ดีที่สุด แต่ในทางปฏิบัติแล้วฉันพบว่ามันง่ายกว่ามากในการจัดการ (โดยเฉพาะเมื่อคุณมีการทดสอบหลายพันครั้ง) ไม่อย่างนั้นคุณกำลังสร้างสคริปต์จำนวนมาก การปฏิบัติจริงมากกว่าความบริสุทธิ์

ฉันจะไม่ต้องสงสัยเลยดูคำตอบนี้ในอีกไม่กี่ปี (เดือน / วัน) และไม่เห็นด้วยกับตัวเองเนื่องจากวิธีการของฉันมีการเปลี่ยนแปลง - แต่นี่เป็นวิธีการปัจจุบันของฉัน

หากต้องการลองและสรุปทุกอย่างที่ฉันได้กล่าวไปข้างต้นนี่คือการทดสอบการรวมฐานข้อมูลโดยทั่วไปของฉัน:

[Test]
public void LoadUser()
{
  this.RunTest(session => // the NH/EF session to attach the objects to
  {
    var user = new UserAccount("Mr", "Joe", "Bloggs");
    session.Save(user);
    return user.UserID;
  }, id => // the ID of the entity we need to load
  {
     var user = LoadMyUser(id); // load the entity
     Assert.AreEqual("Mr", user.Title); // test your properties
     Assert.AreEqual("Joe", user.Firstname);
     Assert.AreEqual("Bloggs", user.Lastname);
  }
}

สิ่งสำคัญที่ควรสังเกตที่นี่คือเซสชันของลูปทั้งสองนั้นเป็นอิสระอย่างสมบูรณ์ ในการติดตั้ง RunTest คุณต้องมั่นใจว่าบริบทมีความมุ่งมั่นและถูกทำลายและข้อมูลของคุณสามารถมาจากฐานข้อมูลของคุณในส่วนที่สองเท่านั้น

แก้ไข 13/10/2557

ฉันบอกว่าฉันอาจจะแก้ไขโมเดลนี้ในอีกไม่กี่เดือนข้างหน้า ในขณะที่ฉันยืนหยัดด้วยวิธีการที่ฉันสนับสนุนด้านบนฉันได้ปรับปรุงกลไกการทดสอบของฉันเล็กน้อย ตอนนี้ฉันมักจะสร้างเอนทิตีใน TestSetup และ TestTearDown

[SetUp]
public void Setup()
{
  this.SetupTest(session => // the NH/EF session to attach the objects to
  {
    var user = new UserAccount("Mr", "Joe", "Bloggs");
    session.Save(user);
    this.UserID =  user.UserID;
  });
}

[TearDown]
public void TearDown()
{
   this.TearDownDatabase();
}

จากนั้นทดสอบคุณสมบัติแต่ละรายการแยกกัน

[Test]
public void TestTitle()
{
     var user = LoadMyUser(this.UserID); // load the entity
     Assert.AreEqual("Mr", user.Title);
}

[Test]
public void TestFirstname()
{
     var user = LoadMyUser(this.UserID);
     Assert.AreEqual("Joe", user.Firstname);
}

[Test]
public void TestLastname()
{
     var user = LoadMyUser(this.UserID);
     Assert.AreEqual("Bloggs", user.Lastname);
}

มีเหตุผลหลายประการสำหรับวิธีนี้:

  • ไม่มีการเรียกฐานข้อมูลเพิ่มเติม (การตั้งค่าเดียวการฉีกขาดหนึ่งครั้ง)
  • การทดสอบมีความละเอียดมากยิ่งขึ้นแต่ละการทดสอบจะตรวจสอบคุณสมบัติหนึ่งรายการ
  • ตรรกะการติดตั้ง / TearDown จะถูกลบออกจากวิธีการทดสอบด้วยตนเอง

ฉันรู้สึกว่าสิ่งนี้ทำให้คลาสการทดสอบง่ายขึ้นและการทดสอบละเอียดยิ่งขึ้น (การยืนยันครั้งเดียวดี )

แก้ไข 5/3/2558

การแก้ไขเกี่ยวกับวิธีการนี้อีก ในขณะที่การตั้งค่าระดับชั้นมีประโยชน์อย่างมากสำหรับการทดสอบเช่นการโหลดคุณสมบัติจะมีประโยชน์น้อยกว่าเมื่อต้องการการตั้งค่าแบบต่างๆ ในกรณีนี้การตั้งค่าคลาสใหม่สำหรับแต่ละกรณีนั้นมากเกินไป

หากต้องการความช่วยเหลือเกี่ยวกับเรื่องนี้ตอนนี้ผมมักจะมีสองชั้นฐานและSetupPerTest SingleSetupสองคลาสเหล่านี้เปิดเผยเฟรมเวิร์กตามต้องการ

ในSingleSetupเรามีกลไกที่คล้ายกันมากตามที่อธิบายไว้ในการแก้ไขครั้งแรกของฉัน ตัวอย่างจะเป็น

public TestProperties : SingleSetup
{
  public int UserID {get;set;}

  public override DoSetup(ISession session)
  {
    var user = new User("Joe", "Bloggs");
    session.Save(user);
    this.UserID = user.UserID;
  }

  [Test]
  public void TestLastname()
  {
     var user = LoadMyUser(this.UserID); // load the entity
     Assert.AreEqual("Bloggs", user.Lastname);
  }

  [Test]
  public void TestFirstname()
  {
       var user = LoadMyUser(this.UserID);
       Assert.AreEqual("Joe", user.Firstname);
  }
}

อย่างไรก็ตามการอ้างอิงซึ่งทำให้แน่ใจว่ามีการโหลดการเข้าใช้ที่ถูกต้องเท่านั้นอาจใช้วิธี SetupPerTest

public TestProperties : SetupPerTest
{
   [Test]
   public void EnsureCorrectReferenceIsLoaded()
   {
      int friendID = 0;
      this.RunTest(session =>
      {
         var user = CreateUserWithFriend();
         session.Save(user);
         friendID = user.Friends.Single().FriendID;
      } () =>
      {
         var user = GetUser();
         Assert.AreEqual(friendID, user.Friends.Single().FriendID);
      });
   }
   [Test]
   public void EnsureOnlyCorrectFriendsAreLoaded()
   {
      int userID = 0;
      this.RunTest(session =>
      {
         var user = CreateUserWithFriends(2);
         var user2 = CreateUserWithFriends(5);
         session.Save(user);
         session.Save(user2);
         userID = user.UserID;
      } () =>
      {
         var user = GetUser(userID);
         Assert.AreEqual(2, user.Friends.Count());
      });
   }
}

โดยสรุปวิธีการทั้งสองทำงานขึ้นอยู่กับสิ่งที่คุณพยายามทดสอบ


2
นี่คือวิธีการทดสอบการรวมระบบที่แตกต่างกัน TL; DR - ใช้แอปพลิเคชันเพื่อตั้งค่าข้อมูลทดสอบย้อนกลับธุรกรรมต่อการทดสอบ
Gert Arnold

3
@Liath การตอบสนองที่ดี คุณยืนยันข้อสงสัยของฉันเกี่ยวกับการทดสอบของ EF คำถามของฉันคือสิ่งนี้; ตัวอย่างของคุณสำหรับกรณีที่เป็นรูปธรรมมากซึ่งใช้ได้ อย่างไรก็ตามตามที่คุณสังเกตคุณอาจต้องทดสอบหลายร้อยรายการ เพื่อให้เป็นไปตามหลักการของ DRY (อย่าทำซ้ำตัวเอง) คุณปรับขนาดโซลูชันของคุณอย่างไรโดยไม่ต้องทำซ้ำรูปแบบรหัสพื้นฐานพื้นฐานทุกครั้งหรือไม่
Jeffrey A. Gochin

4
ฉันต้องไม่เห็นด้วยกับเรื่องนี้เพราะมันเลี่ยงปัญหาโดยสิ้นเชิง การทดสอบหน่วยเป็นเรื่องเกี่ยวกับการทดสอบตรรกะของฟังก์ชัน ในตัวอย่าง OP ตรรกะมีการอ้างอิงในแหล่งข้อมูล คุณพูดถูกเมื่อไม่พูดเพื่อทดสอบ EF แต่นั่นไม่ใช่ปัญหา ปัญหาคือการทดสอบรหัสของคุณแยกออกจากที่เก็บข้อมูล การทดสอบการทำแผนที่ของคุณเป็นหัวข้อที่แตกต่างกันโดยสิ้นเชิง เพื่อทดสอบว่าตรรกะโต้ตอบกับข้อมูลอย่างถูกต้องคุณจะต้องสามารถควบคุมร้านค้าได้
Sinaesthetic

7
ไม่มีใครอยู่ในรั้วเกี่ยวกับว่าคุณควรจะทดสอบหน่วย Entity Framework ด้วยตัวเอง สิ่งที่เกิดขึ้นคือคุณต้องทดสอบวิธีการที่ทำบางสิ่งบางอย่างและเกิดขึ้นกับการโทรไปยังฐานข้อมูลของ EF เป้าหมายคือการเยาะเย้ย EF เพื่อให้คุณสามารถทดสอบวิธีนี้โดยไม่ต้องใช้ฐานข้อมูลบนเซิร์ฟเวอร์การสร้างของคุณ
มัฟฟินแมน

4
ฉันชอบการเดินทางมาก ขอขอบคุณที่เพิ่มการแก้ไขเมื่อเวลาผ่านไป - มันเหมือนกับการควบคุมแหล่งการอ่านและการทำความเข้าใจว่าการคิดของคุณมีวิวัฒนาการอย่างไร ฉันขอขอบคุณความแตกต่างด้านการใช้งาน (กับ EF) และความแตกต่าง (เยาะเย้ย EF) เช่นกัน
Tom Leys

21

ข้อเสนอแนะประสบการณ์ความพยายามที่นี่

หลังจากการอ่านจำนวนมากฉันได้ใช้ความพยายามในการทดสอบของฉัน: ในระหว่างการทดสอบบริบทถูกสร้างขึ้นโดยโรงงานที่ส่งคืนหน่วยความจำในเวอร์ชันซึ่งช่วยให้ฉันทดสอบกับกระดานชนวนว่างเปล่าในแต่ละครั้ง นอกเหนือจากการทดสอบแล้วโรงงานจะแก้ไขเป็นหนึ่งที่ส่งกลับบริบททั้งหมด

อย่างไรก็ตามฉันมีความรู้สึกว่าการทดสอบกับการเยาะเย้ยแบบเต็มรูปแบบของฐานข้อมูลมีแนวโน้มที่จะลากการทดสอบลง; คุณรู้ว่าคุณต้องดูแลการตั้งค่าการพึ่งพาทั้งหมดเพื่อทดสอบส่วนหนึ่งของระบบ นอกจากนี้คุณยังมีแนวโน้มที่จะจัดระเบียบการทดสอบร่วมกันที่อาจไม่เกี่ยวข้องเนื่องจากมีเพียงวัตถุขนาดใหญ่เพียงชิ้นเดียวที่จัดการทุกอย่างได้ หากคุณไม่ใส่ใจคุณอาจพบว่าคุณทำการทดสอบการรวมระบบแทนการทดสอบหน่วย

ฉันต้องการทดสอบกับสิ่งที่เป็นนามธรรมมากกว่า DBContext ขนาดใหญ่ แต่ฉันไม่สามารถหาจุดที่น่าสนใจระหว่างการทดสอบที่มีความหมายและการทดสอบกระดูกเปลือยได้ ชอล์กมันขึ้นอยู่กับประสบการณ์ของฉัน

ดังนั้นฉันพบความพยายามที่น่าสนใจ; หากคุณต้องการใช้งานจริงเป็นเครื่องมือที่ดีในการเริ่มต้นใช้งานและรับผลลัพธ์อย่างรวดเร็ว อย่างไรก็ตามฉันคิดว่าสิ่งที่สวยงามและเป็นนามธรรมควรเป็นขั้นตอนต่อไปและนั่นคือสิ่งที่ฉันจะตรวจสอบต่อไป เพิ่มความนิยมโพสต์นี้เพื่อดูว่ามันจะไปไหนต่อไป :)

แก้ไขเพื่อเพิ่ม : ความพยายามใช้เวลาในการอุ่นเครื่องดังนั้นคุณจึงดูที่ประมาณ 5 วินาทีเมื่อเริ่มต้นการทดสอบ นี่อาจเป็นปัญหาสำหรับคุณหากคุณต้องการให้ชุดทดสอบของคุณมีประสิทธิภาพมาก


แก้ไขเพื่อความกระจ่าง:

ฉันใช้ความพยายามเพื่อทดสอบแอพพลิเคชั่นเว็บเซอร์ แต่ละข้อความ M ที่เข้าสู่ถูกส่งไปยังIHandlerOf<M>ผ่านทางวินด์เซอร์ Castle.Windsor แก้ปัญหาIHandlerOf<M>ที่ resovles พึ่งพาของส่วนประกอบ หนึ่งในสิ่งที่ต้องพึ่งพาเหล่านี้คือDataContextFactoryตัวจัดการซึ่งอนุญาตให้ตัวจัดการร้องขอโรงงาน

ในการทดสอบของฉันฉันยกตัวอย่างชิ้นส่วน IHandlerOf โดยตรงจำลององค์ประกอบย่อยทั้งหมดของ SUT และจัดการห่อหุ้มความพยายามDataContextFactoryไปยังตัวจัดการ

หมายความว่าฉันไม่ได้ทำการทดสอบหน่วยในความหมายที่เข้มงวดเนื่องจากฐานข้อมูลถูกตีด้วยการทดสอบของฉัน อย่างไรก็ตามอย่างที่ฉันบอกไว้ข้างต้นมันทำให้ฉันกระแทกพื้นและฉันสามารถทดสอบบางจุดในแอปพลิเคชันได้อย่างรวดเร็ว


ขอบคุณสำหรับข้อมูลเข้าสิ่งที่ฉันอาจทำตามที่ฉันต้องให้โครงการนี้ทำงานเพราะมันเป็นงานที่ต้องจ่ายให้โดยเริ่มจาก repos และดูว่าฉันจะไปได้อย่างไร แต่ความพยายามนั้นน่าสนใจมาก คุณสนใจที่จะใช้เลเยอร์ใดในการสมัครของคุณ?
Modika

2
เฉพาะในกรณีที่ความพยายามสนับสนุนธุรกรรมอย่างถูกต้อง
Sedat Kapanoglu

และความพยายามมีข้อผิดพลาดสำหรับสตริงที่มีตัวโหลด csv เมื่อเราใช้ '' แทน null ในสตริง
Sam

13

หากคุณต้องการรหัสทดสอบหน่วยคุณต้องแยกรหัสของคุณที่คุณต้องการทดสอบ (ในกรณีนี้คือบริการของคุณ) จากแหล่งข้อมูลภายนอก (เช่นฐานข้อมูล) คุณอาจทำสิ่งนี้กับผู้ให้บริการในหน่วยความจำของ EFอย่างไรก็ตามวิธีที่ใช้กันทั่วไปมากขึ้นก็คือทำให้การใช้งาน EF ของคุณเป็นไปอย่างง่ายดายเช่นด้วยรูปแบบพื้นที่เก็บข้อมูลบางประเภท หากไม่มีการแยกนี้การทดสอบใด ๆ ที่คุณเขียนจะเป็นการทดสอบแบบรวมไม่ใช่การทดสอบหน่วย

สำหรับการทดสอบรหัส EF - ฉันเขียนการทดสอบการรวมอัตโนมัติสำหรับที่เก็บของฉันที่เขียนแถวต่าง ๆ ไปยังฐานข้อมูลในระหว่างการเริ่มต้นแล้วเรียกการใช้งานที่เก็บของฉันเพื่อตรวจสอบให้แน่ใจว่าพวกเขาทำงานตามที่คาดไว้ พวกเขาจะเรียงลำดับที่ถูกต้อง)

สิ่งเหล่านี้เป็นการทดสอบการรวมเข้าด้วยกันไม่ใช่การทดสอบหน่วยเนื่องจากการทดสอบนั้นอาศัยการเชื่อมต่อฐานข้อมูลอยู่และฐานข้อมูลเป้าหมายได้ติดตั้งสคีมาล่าสุดแล้ว


ขอบคุณ @ justin ฉันรู้เกี่ยวกับรูปแบบ Repository แต่การอ่านสิ่งต่าง ๆ เช่นayende.com/blog/4784/ …และlostechies.com/jimmybogard/2009/09/11/wither-the-repositoryท่ามกลางคนอื่นทำให้ฉันคิดว่าฉันไม่ชอบ ไม่ต้องการเลเยอร์สิ่งที่เป็นนามธรรม แต่อีกครั้งสิ่งเหล่านี้พูดคุยเกี่ยวกับวิธีการสืบค้นอีกด้วย
Modika

7
@Modika Ayende ได้เลือกการนำรูปแบบการเก็บข้อมูลไปใช้ในการวิจารณ์ที่ไม่ดีและผลที่ตามมาคือถูก 100% - ผ่านการออกแบบและไม่ได้รับประโยชน์ใด ๆ การใช้งานที่ดีจะแยกส่วนที่ทดสอบได้ของโค้ดของคุณออกจากการใช้ DAL การใช้ NHibernate และ EF โดยตรงทำให้โค้ดยาก (ถ้าไม่เป็นไปไม่ได้) ในการทดสอบหน่วยและนำไปสู่ ​​codebase แบบเสาหินแข็ง ฉันยังค่อนข้างสงสัยเกี่ยวกับรูปแบบพื้นที่เก็บข้อมูล แต่ฉันเชื่อมั่น 100% ว่าคุณต้องแยกการใช้งาน DAL ของคุณอย่างใดและพื้นที่เก็บข้อมูลเป็นสิ่งที่ดีที่สุดที่ฉันเคยพบมา
Justin

2
@Modika อ่านบทความที่สองอีกครั้ง "ฉันไม่ต้องการเลเยอร์สิ่งที่เป็นนามธรรม" ไม่ใช่สิ่งที่เขาพูด อ่านเพิ่มเติมเกี่ยวกับรูปแบบพื้นที่เก็บข้อมูลต้นฉบับจาก Fowler ( martinfowler.com/eaaCatalog/repository.html ) หรือ DDD ( dddcommunity.org/resources/ddd_terms ) อย่าเชื่อ naysayers โดยไม่เข้าใจแนวคิดดั้งเดิมอย่างเต็มที่ สิ่งที่พวกเขาวิพากษ์วิจารณ์จริง ๆ คือการใช้รูปแบบที่ผิดเมื่อเร็ว ๆ นี้ไม่ใช่รูปแบบตัวเอง (แม้ว่าพวกเขาอาจไม่รู้เรื่องนี้)
guillaume31

1
@ guillaume31 ฉันไม่ได้ขัดกับรูปแบบของที่เก็บ (ฉันเข้าใจมัน) ฉันแค่พยายามคิดออกว่าฉันต้องการนามธรรมหรือไม่สิ่งที่เป็นนามธรรมอยู่แล้วในระดับนั้นและถ้าฉันสามารถละเว้นและทดสอบกับ EF โดยตรงโดยการเยาะเย้ย และใช้ในการทดสอบของฉันในระดับที่สูงขึ้นในแอปพลิเคชันของฉัน นอกจากนี้หากฉันไม่ได้ใช้ repo ฉันจะได้รับประโยชน์จากชุดคุณสมบัติเพิ่มเติมของ EF ด้วย repo ที่ฉันอาจไม่ได้รับ
Modika

เมื่อฉันแยก DAL ด้วยที่เก็บฉันต้องใช้วิธี "จำลอง" ฐานข้อมูล (EF) จนถึงตอนนี้การเยาะเย้ยบริบทและส่วนขยาย async ต่างๆ (ToListAsync (), FirstOrDefaultAsync () และอื่น ๆ ) มีผลทำให้ฉันผิดหวัง
เควินเบอร์ตัน

9

ดังนั้นนี่คือสิ่งที่ Entity Framework ดำเนินการดังนั้นแม้ว่าข้อเท็จจริงที่ว่ามันจะสรุปความซับซ้อนของการโต้ตอบของฐานข้อมูลการโต้ตอบโดยตรงยังคงมีเพศสัมพันธ์อย่างแน่นหนาและนั่นเป็นสาเหตุที่ทำให้เกิดความสับสนในการทดสอบ

การทดสอบหน่วยเป็นเรื่องเกี่ยวกับการทดสอบตรรกะของฟังก์ชันและผลลัพธ์ที่เป็นไปได้แต่ละอย่างในการแยกออกจากการอ้างอิงภายนอกใด ๆ ซึ่งในกรณีนี้คือแหล่งข้อมูล ในการดำเนินการดังกล่าวคุณจะต้องสามารถควบคุมพฤติกรรมของแหล่งข้อมูลได้ ตัวอย่างเช่นหากคุณต้องการยืนยันว่าฟังก์ชั่นของคุณคืนค่าเท็จหากผู้ใช้ที่ดึงข้อมูลไม่ตรงตามเกณฑ์บางชุดที่เก็บข้อมูล [mocked] ของคุณควรได้รับการกำหนดค่าให้ส่งคืนผู้ใช้ที่ล้มเหลวตามเกณฑ์เสมอ ในทางกลับกันสำหรับการยืนยันที่ตรงกันข้าม

ด้วยสิ่งที่กล่าวมาและการยอมรับความจริงที่ว่า EF คือการนำไปปฏิบัติฉันอาจจะชอบแนวคิดในการสร้างที่เก็บข้อมูล ดูเหมือนจะซ้ำซ้อนบ้างไหม? ไม่ใช่เพราะคุณกำลังแก้ไขปัญหาที่แยกรหัสของคุณออกจากการใช้ข้อมูล

ใน DDD ที่เก็บจะคืนค่ารากรวมเท่านั้นไม่ใช่ DAO ด้วยวิธีนี้ผู้บริโภคของที่เก็บไม่จำเป็นต้องรู้เกี่ยวกับการใช้ข้อมูล (อย่างที่ควรจะเป็น) และเราสามารถใช้มันเป็นตัวอย่างของวิธีการแก้ปัญหานี้ ในกรณีนี้วัตถุที่สร้างโดย EF คือ DAO และควรถูกซ่อนจากแอปพลิเคชันของคุณ นี่เป็นข้อดีอีกอย่างของที่เก็บที่คุณกำหนด คุณสามารถกำหนดวัตถุธุรกิจเป็นชนิดส่งคืนแทนวัตถุ EF ตอนนี้สิ่งที่ repo ทำคือซ่อนการโทรไปยัง EF และแมปการตอบสนองของ EF กับวัตถุธุรกิจที่กำหนดไว้ในลายเซ็น repos ตอนนี้คุณสามารถใช้ repo นั้นแทนการพึ่งพา DbContext ที่คุณฉีดเข้าไปในคลาสของคุณและดังนั้นตอนนี้คุณสามารถจำลองอินเตอร์เฟสนั้นเพื่อให้การควบคุมที่คุณต้องการเพื่อทดสอบรหัสของคุณโดยแยก

มันใช้งานได้อีกเล็กน้อยและหลาย ๆ คนยกนิ้วให้จมูก แต่มันแก้ปัญหาที่แท้จริงได้ มีผู้ให้บริการในหน่วยความจำที่ถูกกล่าวถึงในคำตอบที่แตกต่างกันซึ่งอาจเป็นตัวเลือก (ฉันไม่ได้ลอง) และการมีอยู่ของมันก็เป็นหลักฐานของความจำเป็นในการฝึกฝน

ฉันไม่เห็นด้วยกับคำตอบที่สมบูรณ์เพราะมันเป็นปัญหาจริงซึ่งแยกรหัสของคุณและจากนั้นไปสัมผัสเกี่ยวกับการทดสอบการทำแผนที่ของคุณ โดยทั้งหมดทดสอบการทำแผนที่ของคุณถ้าคุณต้องการ แต่แก้ไขปัญหาจริงที่นี่และรับความคุ้มครองรหัสจริง


8

ฉันจะไม่ทดสอบรหัสหน่วยฉันไม่ได้เป็นเจ้าของ คุณกำลังทดสอบอะไรที่นี่คอมไพเลอร์ MSFT ทำงานอย่างไร

ที่กล่าวมาเพื่อให้รหัสนี้สามารถทดสอบได้คุณเกือบจะต้องแยกชั้นการเข้าถึงข้อมูลออกจากรหัสตรรกะทางธุรกิจของคุณ สิ่งที่ฉันทำคือนำสิ่งของ EF ทั้งหมดของฉันมาใส่ในคลาส DAO หรือ DAL ซึ่งมีส่วนต่อประสานที่เกี่ยวข้อง จากนั้นฉันเขียนบริการของฉันซึ่งจะมีการฉีด DAO หรือวัตถุ DAL เป็นการอ้างอิง (คอนสตรัคเตอร์การฉีดคอนสตรัคเตอร์) ที่อ้างถึงเป็นอินเทอร์เฟซ ตอนนี้ส่วนที่จะต้องทดสอบ (รหัสของคุณ) สามารถทดสอบได้ง่ายโดยการเยาะเย้ยอินเทอร์เฟซ DAO และฉีดเข้าไปในอินสแตนซ์บริการของคุณในการทดสอบหน่วยของคุณ

//this is testable just inject a mock of IProductDAO during unit testing
public class ProductService : IProductService
{
    private IProductDAO _productDAO;

    public ProductService(IProductDAO productDAO)
    {
        _productDAO = productDAO;
    }

    public List<Product> GetAllProducts()
    {
        return _productDAO.GetAll();
    }

    ...
}

ฉันจะถือว่าเลเยอร์การเข้าถึงข้อมูลสดเป็นส่วนหนึ่งของการทดสอบการรวมเข้าด้วยกันไม่ใช่การทดสอบหน่วย ฉันเคยเห็นพวกทำการตรวจสอบจำนวนการเดินทางไปยังฐานข้อมูลที่จำศีลทำมาก่อน แต่พวกเขาอยู่ในโครงการที่เกี่ยวข้องกับการบันทึกหลายพันล้านรายการในที่เก็บข้อมูลของพวกเขาและการเดินทางพิเศษเหล่านั้นสำคัญมาก


1
ขอบคุณสำหรับคำตอบ แต่ความแตกต่างของสิ่งนี้จะเป็น Repository ที่คุณซ่อน internals ของ EF ที่อยู่ด้านหลังในระดับนี้ ฉันไม่ต้องการให้เป็นนามธรรม EF แม้ว่าฉันจะยังคงทำเช่นนั้นกับส่วนต่อประสาน IContext หรือไม่ ฉันยังใหม่กับสิ่งนี้จงสุภาพ :)
Modika

3
@Modika A Repo ก็ใช้ได้เช่นกัน ไม่ว่าคุณต้องการรูปแบบใด "ฉันไม่ต้องการให้เป็นนามธรรมของ EF" คุณต้องการรหัสที่สามารถทดสอบได้หรือไม่?
Jonathan Henson

1
@Modika จุดของฉันคือคุณจะไม่มีรหัสทดสอบใด ๆ ถ้าคุณไม่แยกข้อกังวลของคุณ การเข้าถึงข้อมูลและตรรกะทางธุรกิจจะต้องอยู่ในชั้นที่แยกต่างหากเพื่อดึงการทดสอบบำรุงรักษาที่ดี
Jonathan Henson

2
ฉันแค่ไม่รู้สึกว่าจำเป็นต้องห่อ EF ไว้ในที่เก็บนามธรรมเพราะ IDbSets เป็น repo และบริบทของ UOW ฉันจะอัปเดตคำถามของฉันเล็กน้อยซึ่งอาจทำให้เข้าใจผิด ปัญหามาพร้อมกับสิ่งที่เป็นนามธรรมและประเด็นหลักคือสิ่งที่ฉันกำลังทดสอบเพราะคิวของฉันจะไม่ทำงานในขอบเขตเดียวกัน (linq-to-entity vs linq-to-objects) ดังนั้นถ้าฉันแค่ทดสอบว่าบริการของฉันทำให้ สายที่ดูเหมือนจะสิ้นเปลืองหรือฉันอยู่ที่นี่ใช่ไหม?
Modika

1
ในขณะที่ฉันเห็นด้วยกับประเด็นทั่วไปของคุณ DbContext เป็นหน่วยของงานและ IDbSets นั้นแน่นอนสำหรับการใช้พื้นที่เก็บข้อมูลและฉันไม่ใช่คนเดียวที่คิด ฉันสามารถเยาะเย้ย EF และในบางเลเยอร์ฉันจะต้องเรียกใช้การทดสอบการรวมกันมันสำคัญมากไหมถ้าฉันทำใน Repository หรือต่อไปใน Service หรือไม่? การเป็นคู่ที่แน่นแฟ้นกับฐานข้อมูลนั้นไม่ได้เป็นข้อกังวลจริงๆฉันแน่ใจว่ามันเกิดขึ้น แต่ฉันจะไม่วางแผนสำหรับบางสิ่งที่อาจไม่เกิดขึ้น
Modika

8

บางครั้งฉันคลำหาข้อพิจารณาเหล่านี้:

1- หากแอปพลิเคชันของฉันเข้าถึงฐานข้อมูลทำไมการทดสอบจึงไม่ควรทำ จะเกิดอะไรขึ้นหากมีสิ่งผิดปกติในการเข้าถึงข้อมูล การทดสอบจะต้องรู้ล่วงหน้าและแจ้งเตือนตัวเองเกี่ยวกับปัญหา

2- รูปแบบพื้นที่เก็บข้อมูลค่อนข้างยากและใช้เวลานาน

ดังนั้นฉันจึงคิดวิธีนี้ขึ้นมาฉันไม่คิดว่าดีที่สุด แต่ทำตามความคาดหวังของฉัน:

Use TransactionScope in the tests methods to avoid changes in the database.

ที่จะทำมันจำเป็น

1- ติดตั้ง EntityFramework ในโครงการทดสอบ 2- ใส่สตริงการเชื่อมต่อลงในไฟล์ app.config ของโครงการทดสอบ 3- อ้างอิง dll System.Transactions ในโครงการทดสอบ

ผลข้างเคียงที่ไม่ซ้ำกันคือเมล็ดข้อมูลเฉพาะตัวจะเพิ่มขึ้นเมื่อพยายามแทรกแม้ว่าธุรกรรมถูกยกเลิก แต่เนื่องจากการทดสอบกับฐานข้อมูลการพัฒนาจึงไม่มีปัญหา

รหัสตัวอย่าง:

[TestClass]
public class NameValueTest
{
    [TestMethod]
    public void Edit()
    {
        NameValueController controller = new NameValueController();

        using(var ts = new TransactionScope()) {
            Assert.IsNotNull(controller.Edit(new Models.NameValue()
            {
                NameValueId = 1,
                name1 = "1",
                name2 = "2",
                name3 = "3",
                name4 = "4"
            }));

            //no complete, automatically abort
            //ts.Complete();
        }
    }

    [TestMethod]
    public void Create()
    {
        NameValueController controller = new NameValueController();

        using (var ts = new TransactionScope())
        {
            Assert.IsNotNull(controller.Create(new Models.NameValue()
            {
                name1 = "1",
                name2 = "2",
                name3 = "3",
                name4 = "4"
            }));

            //no complete, automatically abort
            //ts.Complete();
        }
    }
}

1
จริงๆแล้วฉันชอบวิธีนี้มาก ง่ายสุดในการปรับใช้และสถานการณ์การทดสอบที่สมจริงยิ่งขึ้น ขอบคุณ!
slopapa

1
ด้วย EF 6 คุณจะใช้ DbContext.Database.BeginTransaction ใช่ไหม
SwissCoder

5

ในระยะสั้นฉันจะบอกว่าไม่มีน้ำผลไม้ไม่คุ้มค่ากับการบีบวิธีทดสอบบริการด้วยบรรทัดเดียวที่ดึงข้อมูลโมเดล จากประสบการณ์ของฉันผู้ที่ยังใหม่กับ TDD ต้องการทดสอบทุกอย่างอย่างแน่นอน เกาลัดเก่าของการทำให้ส่วนหน้าของเฟรมเวิร์กของบุคคลที่สามเป็นจริงเพื่อให้คุณสามารถสร้างการจำลองของ API เฟรมเวิร์กที่คุณใช้ไอเท็ม / ขยายเพื่อให้คุณสามารถฉีดข้อมูลดัมมีความสำคัญในใจของฉัน ทุกคนมีมุมมองที่แตกต่างกันว่าการทดสอบหน่วยดีที่สุดเท่าไหร่ ฉันมักจะเน้นการปฏิบัติมากขึ้นในวันนี้และถามตัวเองว่าการทดสอบของฉันเพิ่มคุณค่าให้กับผลิตภัณฑ์สุดท้ายหรือไม่


1
ใช่เพื่อปฏิบัตินิยม ฉันยังคงยืนยันว่าคุณภาพการทดสอบหน่วยของคุณต่ำกว่าคุณภาพของรหัสต้นฉบับของคุณ แน่นอนว่ามันมีค่าในการใช้ TDD เพื่อปรับปรุงการเข้ารหัสของคุณและเพื่อปรับปรุงการบำรุงรักษา แต่ TDD สามารถลดค่าได้ เราดำเนินการทดสอบทั้งหมดของเรากับฐานข้อมูลเพราะมันทำให้เรามั่นใจว่าการใช้งาน EF และตารางตัวเองนั้นฟังดูดี การทดสอบใช้เวลานานกว่าในการรัน แต่มันมีความน่าเชื่อถือมากกว่า
Savage

3

ฉันต้องการแบ่งปันวิธีการแสดงความคิดเห็นและพูดคุยสั้น ๆ แต่แสดงตัวอย่างจริงที่ฉันใช้ในปัจจุบันเพื่อช่วยทดสอบบริการต่าง ๆ ของ EF

อันดับแรกฉันชอบที่จะใช้ผู้ให้บริการในหน่วยความจำจาก EF Core แต่นี่เป็นเรื่องเกี่ยวกับ EF 6 นอกจากนี้สำหรับระบบจัดเก็บข้อมูลอื่น ๆ เช่น RavenDB ฉันยังเป็นผู้สนับสนุนการทดสอบผ่านผู้ให้บริการฐานข้อมูลในหน่วยความจำ อีกครั้ง - นี่เป็นพิเศษเพื่อช่วยทดสอบรหัสที่ใช้ EF โดยไม่ต้องทำพิธีมากมาย

นี่คือเป้าหมายที่ฉันมีเมื่อคิดแบบ:

  • มันจะต้องง่ายสำหรับนักพัฒนาคนอื่น ๆ ในทีมที่จะเข้าใจ
  • มันจะต้องแยกรหัส EF ในระดับที่เป็นไปได้น้อยที่สุด
  • ต้องไม่เกี่ยวข้องกับการสร้างส่วนต่อประสานที่มีความรับผิดชอบแปลก ๆ (เช่นรูปแบบที่เก็บข้อมูล "ทั่วไป" หรือ "ทั่วไป")
  • จะต้องง่ายต่อการกำหนดค่าและตั้งค่าในการทดสอบหน่วย

ฉันเห็นด้วยกับข้อความก่อนหน้านี้ว่า EF ยังคงมีรายละเอียดการใช้งานและรู้สึกดีที่คุณต้องการสรุปเพื่อทำการทดสอบหน่วย "บริสุทธิ์" ฉันยังยอมรับว่านึกคิดฉันต้องการเพื่อให้แน่ใจว่ารหัส EF ตัวเองทำงาน - แต่นี้จะเกี่ยวข้องกับฐานข้อมูล sandbox ของผู้ให้บริการในหน่วยความจำ ฯลฯ แก้ปัญหาวิธีการของฉันทั้งสอง - คุณสามารถได้อย่างปลอดภัยการทดสอบหน่วย EF-ขึ้นอยู่กับรหัสและสร้าง การทดสอบการรวมเพื่อทดสอบรหัส EF ของคุณโดยเฉพาะ

วิธีที่ฉันประสบความสำเร็จคือเพียงแค่ใส่รหัส EFใส่ลงในคลาส Query และ Command โดยเฉพาะ ความคิดนั้นง่าย: เพียงแค่ห่อโค้ด EF ใด ๆ ในคลาสและขึ้นอยู่กับอินเทอร์เฟซในชั้นเรียนที่จะใช้ในตอนแรก ปัญหาหลักที่ฉันต้องแก้ไขคือการหลีกเลี่ยงการเพิ่มการอ้างอิงจำนวนมากให้กับชั้นเรียนและตั้งค่ารหัสจำนวนมากในการทดสอบของฉัน

ซึ่งเป็นที่ที่มีประโยชน์ห้องสมุดที่เรียบง่ายมาใน: Mediatr มันช่วยให้การส่งข้อความในกระบวนการง่ายและทำได้โดยการแยก "คำขอ" จากตัวจัดการที่ใช้รหัส สิ่งนี้มีประโยชน์เพิ่มเติมในการแยก "อะไร" จาก "วิธี" ตัวอย่างเช่นการห่อหุ้มรหัส EF เป็นชิ้นเล็ก ๆ จะช่วยให้คุณสามารถแทนที่การใช้งานด้วยผู้ให้บริการรายอื่นหรือกลไกที่แตกต่างกันโดยสิ้นเชิงเพราะสิ่งที่คุณทำคือการส่งคำขอเพื่อดำเนินการ

การใช้การฉีดพึ่งพา (มีหรือไม่มีกรอบ - ความต้องการของคุณ) เราสามารถเยาะเย้ยผู้ไกล่เกลี่ยและควบคุมกลไกการร้องขอ / ตอบสนองเพื่อเปิดใช้งานการทดสอบหน่วย EF

อันดับแรกสมมติว่าเรามีบริการที่มีตรรกะทางธุรกิจที่เราต้องทดสอบ:

public class FeatureService {

  private readonly IMediator _mediator;

  public FeatureService(IMediator mediator) {
    _mediator = mediator;
  }

  public async Task ComplexBusinessLogic() {
    // retrieve relevant objects

    var results = await _mediator.Send(new GetRelevantDbObjectsQuery());
    // normally, this would have looked like...
    // var results = _myDbContext.DbObjects.Where(x => foo).ToList();

    // perform business logic
    // ...    
  }
}

คุณเริ่มเห็นประโยชน์ของวิธีนี้หรือไม่? ไม่เพียงแค่คุณใส่รหัสที่เกี่ยวข้องกับ EF ทั้งหมดลงในคลาสที่เป็นคำอธิบายอย่างชัดเจนเท่านั้นคุณยังสามารถเพิ่มความสามารถในการขยายได้โดยการลบข้อกังวลเกี่ยวกับการปฏิบัติตาม "วิธี" คำขอนี้ - คลาสนี้ไม่สนใจว่าวัตถุที่เกี่ยวข้องมาจาก EF, MongoDB หรือไฟล์ข้อความ

ตอนนี้สำหรับการร้องขอและการจัดการผ่าน MediatR:

public class GetRelevantDbObjectsQuery : IRequest<DbObject[]> {
  // no input needed for this particular request,
  // but you would simply add plain properties here if needed
}

public class GetRelevantDbObjectsEFQueryHandler : IRequestHandler<GetRelevantDbObjectsQuery, DbObject[]> {
  private readonly IDbContext _db;

  public GetRelevantDbObjectsEFQueryHandler(IDbContext db) {
    _db = db;
  }

  public DbObject[] Handle(GetRelevantDbObjectsQuery message) {
    return _db.DbObjects.Where(foo => bar).ToList();
  }
}

อย่างที่คุณเห็นสิ่งที่เป็นนามธรรมนั้นง่ายและถูกห่อหุ้ม นอกจากนี้ยังสามารถทดสอบได้อย่างแน่นอนเพราะในการทดสอบการรวมคุณสามารถทดสอบชั้นนี้ทีละ - ไม่มีความกังวลทางธุรกิจผสมอยู่ที่นี่

ดังนั้นการทดสอบหน่วยของบริการคุณลักษณะของเราจะเป็นอย่างไร มันง่ายมาก ในกรณีนี้ฉันใช้Moqในการเยาะเย้ย (ใช้สิ่งที่ทำให้คุณมีความสุข):

[TestClass]
public class FeatureServiceTests {

  // mock of Mediator to handle request/responses
  private Mock<IMediator> _mediator;

  // subject under test
  private FeatureService _sut;

  [TestInitialize]
  public void Setup() {

    // set up Mediator mock
    _mediator = new Mock<IMediator>(MockBehavior.Strict);

    // inject mock as dependency
    _sut = new FeatureService(_mediator.Object);
  }

  [TestCleanup]
  public void Teardown() {

    // ensure we have called or expected all calls to Mediator
    _mediator.VerifyAll();
  }

  [TestMethod]
  public void ComplexBusinessLogic_Does_What_I_Expect() {
    var dbObjects = new List<DbObject>() {
      // set up any test objects
      new DbObject() { }
    };

    // arrange

    // setup Mediator to return our fake objects when it receives a message to perform our query
    // in practice, I find it better to create an extension method that encapsulates this setup here
    _mediator.Setup(x => x.Send(It.IsAny<GetRelevantDbObjectsQuery>(), default(CancellationToken)).ReturnsAsync(dbObjects.ToArray()).Callback(
    (GetRelevantDbObjectsQuery message, CancellationToken token) => {
       // using Moq Callback functionality, you can make assertions
       // on expected request being passed in
       Assert.IsNotNull(message);
    });

    // act
    _sut.ComplexBusinessLogic();

    // assertions
  }

}

คุณสามารถเห็นทุกสิ่งที่เราต้องการคือการตั้งค่าเพียงครั้งเดียวและเราไม่จำเป็นต้องกำหนดค่าอะไรเพิ่มเติม - เป็นการทดสอบหน่วยที่ง่ายมาก มีความชัดเจน:นี่เป็นไปได้ทั้งหมดที่จะทำโดยไม่ต้องมี Mediatr (คุณเพียงแค่สร้างส่วนต่อประสานและจำลองการทดสอบIGetRelevantDbObjectsQuery) แต่ในทางปฏิบัติสำหรับ codebase ขนาดใหญ่ที่มีคุณสมบัติและคำสั่ง / คำสั่งมากมายฉันชอบ encapsulation และ สนับสนุนโดยธรรมชาติ DI ข้อเสนอของ Mediatr

หากคุณสงสัยว่าฉันจัดการชั้นเรียนเหล่านี้ได้อย่างไรมันค่อนข้างง่าย:

- MyProject
  - Features
    - MyFeature
      - Queries
      - Commands
      - Services
      - DependencyConfig.cs (Ninject feature modules)

การจัดระเบียบตามฟีเจอร์สจะอยู่ด้านข้างจุด แต่จะทำให้โค้ดที่เกี่ยวข้อง / ขึ้นกับกันทั้งหมดและค้นพบได้ง่าย สิ่งสำคัญที่สุดคือฉันแยกแบบสอบถามกับคำสั่ง - ตามหลักการแยกคำสั่ง / แบบสอบถาม

สิ่งนี้ตรงตามเกณฑ์ทั้งหมดของฉัน: เป็นงานที่ต่ำเข้าใจง่ายและมีประโยชน์ซ่อนเร้นเป็นพิเศษ ตัวอย่างเช่นคุณจัดการกับการเปลี่ยนแปลงการบันทึกได้อย่างไร ตอนนี้คุณสามารถทำให้บริบท Db ของคุณง่ายขึ้นโดยใช้ส่วนต่อประสานบทบาท (IUnitOfWork.SaveChangesAsync()) และจำลองการโทรไปที่อินเทอร์เฟซบทบาทเดียวหรือคุณสามารถสรุปแค็ปซูลคอมมิชชัน / ย้อนกลับภายใน RequestHandlers ของคุณ - อย่างไรก็ตามคุณต้องการที่จะทำมันขึ้นอยู่กับคุณตราบใดที่มันสามารถบำรุงรักษาได้ ตัวอย่างเช่นฉันถูกล่อลวงให้สร้างคำขอทั่วไป / ผู้ดำเนินการเดียวที่คุณเพิ่งผ่านวัตถุ EF และมันจะบันทึก / อัปเดต / ลบมัน - แต่คุณต้องถามว่าความตั้งใจของคุณคืออะไรและจำไว้ว่าถ้าคุณต้องการ สลับผู้จัดการกับผู้ให้บริการจัดเก็บ / การนำไปใช้อีกรายคุณควรสร้างคำสั่ง / แบบสอบถามที่ชัดเจนซึ่งแสดงถึงสิ่งที่คุณตั้งใจจะทำ บ่อยครั้งที่บริการหรือฟีเจอร์เดียวจะต้องมีบางอย่างที่เฉพาะเจาะจง - อย่าสร้างสิ่งทั่วไปก่อนที่คุณจะต้องการมัน

มีข้อแม้แน่นอนสำหรับรูปแบบนี้ - คุณสามารถไปไกลเกินไปด้วยกลไกแบบผับ / ย่อยอย่างง่าย ฉันได้ จำกัด การนำไปใช้ของฉันให้เป็นนามธรรมเฉพาะโค้ดที่เกี่ยวข้องกับ EF เท่านั้น แต่ผู้พัฒนาที่ชอบการผจญภัยสามารถเริ่มใช้ MediatR เพื่อลงน้ำและทำทุกอย่างให้เป็นข้อความ นั่นเป็นปัญหาของกระบวนการไม่ใช่ปัญหากับ MediatR ดังนั้นจงรับทราบว่าคุณใช้รูปแบบนี้อย่างไร

คุณต้องการตัวอย่างที่ชัดเจนว่าผู้คนทดสอบหน่วย / เยาะเย้ย EF อย่างไรและนี่คือวิธีการที่ทำงานอย่างประสบความสำเร็จสำหรับเราในโครงการของเรา - และทีมมีความสุขสุด ๆ กับการนำมาใช้ง่าย ฉันหวังว่านี่จะช่วยได้! เช่นเดียวกับทุกสิ่งในการเขียนโปรแกรมมีหลายวิธีและทั้งหมดขึ้นอยู่กับสิ่งที่คุณต้องการบรรลุ ฉันให้ความสำคัญกับความเรียบง่ายใช้งานง่ายบำรุงรักษาและการค้นพบ - และโซลูชันนี้ตอบสนองความต้องการเหล่านั้นทั้งหมด


ขอบคุณสำหรับคำตอบมันเป็นคำอธิบายที่ดีของรูปแบบ QueryObject โดยใช้คนกลางและสิ่งที่ฉันเริ่มที่จะผลักดันในโครงการของฉันเช่นกัน ฉันอาจต้องอัปเดตคำถาม แต่ฉันไม่ได้ทำการทดสอบหน่วย EF อีกต่อไปแล้ว abstractions นั้นรั่วไหลเกินไป (SqlLite อาจไม่เป็นไร) ดังนั้นฉันจึงรวมการทดสอบสิ่งที่ถามกฎฐานข้อมูลและการทดสอบหน่วยธุรกิจและตรรกะอื่น ๆ
Modika

3

มีความพยายามซึ่งเป็นผู้ให้บริการฐานข้อมูลหน่วยงานกรอบหน่วยความจำ ฉันไม่ได้ลองจริงๆ ... ฮ่า ๆ เพิ่งเห็นสิ่งนี้ถูกกล่าวถึงในคำถาม!

อีกวิธีหนึ่งคือคุณสามารถสลับไปใช้ EntityFrameworkCore ซึ่งมีผู้ให้บริการฐานข้อมูลหน่วยความจำในตัว

https://blog.goyello.com/2016/07/14/save-time-mocking-use-your-real-entity-framework-dbcontext-in-unit-tests/

https://github.com/tamasflamich/effort

ฉันใช้โรงงานเพื่อรับบริบทดังนั้นฉันจึงสามารถสร้างบริบทที่ใกล้เคียงกับการใช้งานได้ สิ่งนี้ดูเหมือนว่าจะทำงานในท้องถิ่นใน visual studio แต่ไม่ได้อยู่บนเซิร์ฟเวอร์สร้าง TeamCity ของฉันไม่แน่ใจว่าทำไม

return new MyContext(@"Server=(localdb)\mssqllocaldb;Database=EFProviders.InMemory;Trusted_Connection=True;");

สวัสดีแอนดรูว์ปัญหาไม่เคยได้รับบริบทคุณสามารถสร้างบริบทซึ่งเป็นสิ่งที่เรากำลังทำสรุปบริบทและสร้างขึ้นโดยโรงงาน ปัญหาที่ใหญ่ที่สุดคือความสอดคล้องของสิ่งที่อยู่ในหน่วยความจำกับสิ่งที่ Linq4Entities ทำพวกเขาไม่เหมือนกันซึ่งอาจนำไปสู่การทดสอบที่ทำให้เข้าใจผิด ขณะนี้เราเพิ่งรวมฐานข้อมูลทดสอบอาจไม่ใช่กระบวนการที่ดีที่สุดสำหรับทุกคนที่สนใจ
Modika

เครื่องมือช่วย Moq นี้ใช้งานได้ ( codeproject.com/Tips/1045590/… ) หากคุณมีบริบทที่จะเยาะเย้ย หากคุณสำรองข้อมูลบริบทที่เยาะเย้ยด้วยรายการจะไม่ทำงานเหมือนบริบทที่ได้รับการสนับสนุนโดยฐานข้อมูล sql
andrew pate

2

ฉันชอบที่จะแยกตัวกรองของฉันออกจากส่วนอื่น ๆ ของรหัสและทดสอบตัวกรองตามที่ฉันเขียนไว้ที่บล็อกของฉันที่นี่http://coding.grax.com/2013/08/testing-custom-linq-filter-operators.html

ตรรกะตัวกรองที่ถูกทดสอบนั้นไม่เหมือนกับตรรกะตัวกรองที่เรียกใช้เมื่อโปรแกรมทำงานเนื่องจากการแปลระหว่างนิพจน์ LINQ และภาษาคิวรีพื้นฐานเช่น T-SQL ยังคงนี้ช่วยให้ฉันสามารถตรวจสอบตรรกะของตัวกรอง ฉันไม่กังวลมากเกินไปเกี่ยวกับการแปลที่เกิดขึ้นและสิ่งต่าง ๆ เช่นการพิจารณาตัวพิมพ์เล็กและตัวจัดการแบบ null จนกว่าฉันจะทดสอบการรวมระหว่างเลเยอร์


0

มันเป็นสิ่งสำคัญที่จะทดสอบสิ่งที่คุณคาดหวังว่ากรอบงานของเอนทิตีจะต้องทำ (เช่นตรวจสอบความคาดหวังของคุณ) วิธีหนึ่งในการทำสิ่งนี้ที่ฉันใช้สำเร็จแล้วคือใช้ moq ดังที่แสดงในตัวอย่างนี้ (ยาวเพื่อคัดลอกไปยังคำตอบนี้):

https://docs.microsoft.com/en-us/ef/ef6/fundamentals/testing/mocking

อย่างไรก็ตามโปรดระวัง ... บริบท SQL ไม่รับประกันว่าจะส่งคืนสิ่งต่าง ๆ ตามลำดับที่ระบุเว้นแต่ว่าคุณมี "OrderBy" ที่เหมาะสมในการสอบถาม linq ของคุณดังนั้นจึงเป็นไปได้ที่จะเขียนสิ่งที่ผ่านเมื่อคุณทดสอบโดยใช้รายการในหน่วยความจำ linq-to-entity) แต่ล้มเหลวในสภาพแวดล้อม uat / live ของคุณเมื่อมีการใช้ (linq-to-sql)

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.