[Class factory / C#] Generic class factory met unit testen

Pagina: 1
Acties:

Onderwerpen


Acties:
  • 0 Henk 'm!

  • Jan_V
  • Registratie: Maart 2002
  • Laatst online: 22:19
Vorige week kwam ik deze post tegen waar een generic class factory wordt gepresenteerd: http://aabs.wordpress.com...c-class-factory-for-c-30/
Voorheen maakte de schrijver voor iedere klasse een stuk code als deze:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyClassClassFactory
{
    public class Environment
    {
        public static bool InUnitTest { get; set; }
        public static IMyClass ObjectToDispense { get; set; }
    }

    public static IMyClass CreateInstance()
    {
        if (Environment.InUnitTest)
            return Environment.ObjectToDispense;
        return new MyClass() as IMyClass;
    }
}

Dit heeft hij nu vervangen met een generieke klasse als deze:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
public static class GenericClassFactory<I, C>
    where C : I
{
    public static Func<object[], I> Dispenser { get; set; }

    public static I CreateInstance(params object[] args)
    {
        if (Dispenser != null)
            return Dispenser(args);
        return (I) Activator.CreateInstance(typeof (C), args);
    }
}

Een stuk compacter en natuurlijk generiek.
In z'n uitleg geeft hij nog enkele voorbeelden over gebruik en wat er mogelijk is, maar er is 1 ding wat ik niet helemaal begrijp.

In het eerste voorbeeld wordt er expliciet gekeken of er wordt getest, zo ja, dan wordt er (mogelijk) een ander object terug gestuurd. Dit is niet zo in het 2e voorbeeld, de generieke oplossing.

Volgens mij moet er nog iets worden toegevoegd voor het gebruik binnen unit tests, om zo een gemockt object terug te kunnen geven.

Nu ik deze post hier schrijf besef ik me ineens dat dit mogelijk al is geimplementeerd door middel van de Dispatcher te kunnen setten:
C#:
1
2
3
GenericClassFactory<IMyClass, MyClass>.Dispenser = (args) =>
        (IMyClass)Activator.CreateInstance(typeof(MyClass2), args);
    IMyClass ad = GenericClassFactory<IMyClass, MyClass>.CreateInstance(1, 2, 3)

Is dat inderdaad het geval? Wordt hier de dispatcher geset, zodat je met bijvoorbeeld unit tests je gemockte MyClass2 terug krijgt?

Zit ik nog wel met het vraagstuk, blijft de Dispatcher voor de betreffende interface altijd gelden? Volgens mij zou ik in m'n TestMethod dan de Dispatcher moeten vullen met een mock object, maar pas in de te testen methode wordt het object gemaakt. Wellicht omdat er hier static methoden worden gebruikt dat hij dan 'onthoudt' dat je een MyClass2 terug wilt krijgen, maar dat weet ik niet 100% zeker.

Als deze class factory werkt zoals ik denk dat hij werkt, dan lijkt me dit een redelijk chique methode om alles te implementeren.

Battle.net - Jandev#2601 / XBOX: VriesDeJ


Acties:
  • 0 Henk 'm!

Verwijderd

Code moeten schrijven om een unit test mogelijk te maken in je echte implementatie is natuurlijk sowieso niet de bedoeling. Ongeacht de implementatie zoals hij gekozen is zal dit nooit goede betrouwbare unit tests opleveren. Er is namelijk geen test die test of de code die geschreven is voor de test de code voor productie niet in de weg staat en zo fouten veroorzaakt.

Wat de auteur had moeten doen was interfaces gebruiken op zowel de class als de factory en op die manier een unittest factory kunnen mocken die unittest classes terug gaf.

Dit zouden betere oplossingen zijn:

C#:
1
2
3
4
5
6
7
8
public static class GenericClassFactory<C> 
    where C : IUnitTestableClass, new()
{ 
    public static IUnitTestableClass CreateInstance(params object[] args) 
    { 
        return new C();
    } 
} 


of zelfs onderstaande implementatie, waarbij zowel de factory als de class verwisseld kunnen worden met stubs mits de interfaces gebruikt worden in de rest van de code.
C#:
1
2
3
4
5
6
7
8
public class GenericClassFactory<C> : IUnitTestableClassFactory<C>
    where C : IUnitTestableClass, new()
{ 
    public IUnitTestableClass CreateInstance(params object[] args) 
    { 
        return new C();
    } 
} 

[ Voor 38% gewijzigd door Verwijderd op 15-12-2010 09:57 ]


Acties:
  • 0 Henk 'm!

  • Sebazzz
  • Registratie: September 2006
  • Laatst online: 19:03

Sebazzz

3dp

Verwijderd schreef op woensdag 15 december 2010 @ 09:53:
Code moeten schrijven om een unit test mogelijk te maken in je echte implementatie is natuurlijk sowieso niet de bedoeling.
Dat vind ik een erg interessant statement. Hoe wil je dependency injection doen zonder code te schrijven in je code om unit tests mogelijk te maken?

Voorbeeldje (grof) in ASP.NET MVC:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// Model/IProductRepository.cs
public interface IProductRepository {
    Product GetProduct(int id);

    IEnumerable<Product> GetProductsAddedBy(string username);

    // many more
}

// Model/ProductRepository.cs
public class ProductRepository : IProductRepository{
    private readonly ProductDataContext db; // Linq to Sql

    public ProductRepository() {
         this.db = new ProductDataContext();
    }
    
    public Product GetProduct(int id) { 
        return this.db.SingleOrDefault(p => p.Id == id);
    }

    public IEnumerable<Product> GetProductsAddedBy(string username) {
        return this.db.SingleOrDefault(p => p.AddedBy == username);
    }

    // many more ...
}

// Controller/ProductController.cs
public class ProductController : Controller {
      private readonly IProductRepository productRepository;

      public ProductController() {
          this.productRepository = new ProductRepository();
      }

      public ProductController(IProductRepository productRepo) {
          this.productRepository = productRepo;
      }

      // action methods
}


In je unit test zou je dan een object van type ProductController aanmaken en een object met de unit-test implementatie van de IProductRepository interface meegeven.

[Te koop: 3D printers] [Website] Agile tools: [Return: retrospectives] [Pokertime: planning poker]


Acties:
  • 0 Henk 'm!

Verwijderd

Sebazzz schreef op woensdag 15 december 2010 @ 10:07:
[...]
Dat vind ik een erg interessant statement. Hoe wil je dependency injection doen zonder code te schrijven in je code om unit tests mogelijk te maken?

Voorbeeldje (grof) in ASP.NET MVC:
C#:
1
...


In je unit test zou je dan een object van type ProductController aanmaken en een object met de unit-test implementatie van de IProductRepository interface meegeven.
Ik bedoel het iets anders inderdaad ;): Ik bedoel dat je je unittest code niet moet mixen met je "productie"-code. Je stub moet dus niet in je echte classes zitten zoals in het gegeven voorbeeld wel gebeurd. Met Dependency Injection haal je dit dus netjes uit elkaar.

Om DI echter te kunnen gebruiken moet het echter natuurlijk wel onderdeel vormen van je code, zelfs als je het zou doen met AOP (Naar mijn mening is ook attributen zetten op classes/methoden/properties gewoon een onderdeel van het coderen).

[ Voor 35% gewijzigd door Verwijderd op 15-12-2010 10:14 ]


Acties:
  • 0 Henk 'm!

  • Woy
  • Registratie: April 2000
  • Niet online

Woy

Moderator Devschuur®
Het is inderdaad gek om in je productie code direct onderscheid te maken voor Unit Tests. Je code moet alleen zo opgezet zijn dat bepaalde delen ( b.v. d.m.v DI ( Zo dat waren weer even alle afkortingen ;) ) voor een Unit Test vervangen kunnen worden.

Het eerste code voorbeeld is dan sowieso gek, aangezien daar een constante class terug gegeven word bij Productie. Het is dan natuurlijk veel makkelijker om het geheel helemaal met DI te laten werken, en zo voor elke situatie te kunnen kiezen welke implementatie er gebruikt word. Dan heb je zowel de Unit Test case als de Productie case afgevangen.

“Build a man a fire, and he'll be warm for a day. Set a man on fire, and he'll be warm for the rest of his life.”


Acties:
  • 0 Henk 'm!

  • Jan_V
  • Registratie: Maart 2002
  • Laatst online: 22:19
Verwijderd schreef op woensdag 15 december 2010 @ 09:53:
C#:
1
2
3
4
5
6
7
8
public static class GenericClassFactory<C> 
    where C : IUnitTestableClass, new()
{ 
    public static IUnitTestableClass CreateInstance(params object[] args) 
    { 
        return new C();
    } 
} 
Is bovenstaande voorbeeld niet bijna hetzelfde als dit:
C#:
1
2
3
4
5
6
7
8
public static class GenericClassFactory<I, C> 
    where T : class, I, new()
{ 
    public static I CreateInstance(params object[] args) 
    { 
        return Activator.CreateInstance(typeof (C), args) as I;
    } 
}

Het verschil is dat de Interface nu moet worden meegegeven.
Het lijkt me dat je dat ook wilt, anders moet er een of andere BaseInterface worden geschreven die niet direct waarde heeft.
Op deze manier kun je ook gewoon gemockte objecten als C(lass) meegeven, zolang ze maar aan I(nterface) voldoen. Iets wat je uiteraard ook wilt bij een gemockt object.
Was nog even aan het rondneuzen of je ook expliciet op kan geven of een generic paramater ook een interface moet zijn, maar zo te zien kan dat niet, enkel (abstracte) klassen.

Of ik je tweede voorbeeld (interface op de factory method) nou ook handig vind moet ik even een nachtje over slapen. Momenteel denk ik van niet, maar wellicht denk ik daar morgen anders over.

Battle.net - Jandev#2601 / XBOX: VriesDeJ


Acties:
  • 0 Henk 'm!

Verwijderd

Jan_V schreef op woensdag 15 december 2010 @ 19:29:
[...]

Is bovenstaande voorbeeld niet bijna hetzelfde als dit:
C#:
1
2
3
4
5
6
7
8
public static class GenericClassFactory<I, C> 
    where T : class, I, new()
{ 
    public static I CreateInstance(params object[] args) 
    { 
        return Activator.CreateInstance(typeof (C), args) as I;
    } 
}

Het verschil is dat de Interface nu moet worden meegegeven.
Het lijkt me dat je dat ook wilt, anders moet er een of andere BaseInterface worden geschreven die niet direct waarde heeft.
Op deze manier kun je ook gewoon gemockte objecten als C(lass) meegeven, zolang ze maar aan I(nterface) voldoen. Iets wat je uiteraard ook wilt bij een gemockt object.
Was nog even aan het rondneuzen of je ook expliciet op kan geven of een generic paramater ook een interface moet zijn, maar zo te zien kan dat niet, enkel (abstracte) klassen.

Of ik je tweede voorbeeld (interface op de factory method) nou ook handig vind moet ik even een nachtje over slapen. Momenteel denk ik van niet, maar wellicht denk ik daar morgen anders over.
Dat is vooral een persoonlijke keuze die iedereen voor zich moet maken.

Echter is het wel zo dat zodra je Activor.CreateInstance in je code gaat gebruiken om factories/classen te instantieren binnen een class die zelf al generic is, je eigenlijk al iets moeilijker doet dan het is. Bovendien is in het bovenstaande voorbeeld C een afgeleide van I en kun je dus prima met alleen I af en hoef je C niet mee te geven.

Tweede punt met de bovenstaande code is, is dat het helemaal niet boeit dat C een afgeleide van I is. Hij kan C prima instantiëren zonder van I af te weten. Immers (even versimpeld zonder echte methode uitwerking, ik hoop dat je het zo ook begrijpt):
C#:
1
GenericClassFactory<ITest, Test>.CreateInstance(null);

doet net zoveel als en geeft hetzelfde resultaat als:
C#:
1
GenericClassFactory<Test>.CreateInstance(null);

en ook:
C#:
1
GenericClassFactory<ITest, MockTest>.CreateInstance(null);

doet net zoveel als en geeft hetzelfde resultaat als:
C#:
1
GenericClassFactory<MockTest>.CreateInstance(null);


Bedenk ook dat het maken van baseclasses en interfaces een best practise. Het is dus absoluut geen zonde om die aan te maken. Het is vaak zelfs beter, omdat je code stricter wordt en je zaken als Activator.CreateInstance kunt vermijden wanneer dat nodig is.
Zie onder andere:
http://www.codethinked.co...Instance-Performance.aspx
http://bloggingabout.net/...d-lambda-expressions.aspx

Tenslotte: Stel je zelf ook de vraag hoe generiek je code moet worden. Te generiek maakt de code te complex en schiet zijn doel voorbij. In dit geval lijkt dat ook het geval te zijn, want je kunt vanuit elke class dan naar een interface proberen te casten. Dat is vast geen requirement in je applicatie en de requirements die er wel voor zijn, kun je ook op minder generieke snellere en overzichtelijkere manieren (zoals in mijn voorbeelden) oplossen.
Pagina: 1