Click here to Skip to main content
15,890,282 members
Articles / Programming Languages / C#
Article

Auto test all the Enum types for duplicated values and error-prone Flags values using Reflection

Rate me:
Please Sign up or sign in to vote.
1.80/5 (2 votes)
13 Jul 20075 min read 30.4K   109   9  
Auto test all the Enum types for duplicated values and error-prone Flags values using Reflection

Introduction

There's so many Enum definition in a real world C#/.NET project. And I found that in many case we need each enum item has distinct value from others, but the real truth is that chances exists developer define more than one enum item by assign them the same value. That's the source of bug. And, to make things further interesting, there's [Flags] Enum, which value should be 0x1, 0x2, 0x4 etc, while not something 0x7. This article illustrate the reusable way to check these Enum-definition related things automatically by Reflection. And apply these rules to an unit test framework is easy, such as NUnit.

Screenshot - nunit_test_enum.png

Background

Before you can use NUnit test in the code, you should include using NUnit.Framework; in the preamble. And

Add a reference to the NUnit dll: nunit-framework.dll, just this one is really needed.

Test on by C# on .NET 1.1

The demo project itself is not runnable, just use NUnit to test it.

Motivation

Reflection on .NET open a door to many interesting things, in fact, the motivation of this article lies in another story:

Localization for all the name of an enum:

C#
public enum Unit
{
  mm,
  cm,
  pt,
  Inch,
}
That's very common in a drawing software. And I want to localize these item in a string table, just keep the string table nearest to the enum is not sufficient for sync the two things. I need to ensure that my software will notify the developer if someone add a new supported unit to enum Unit while leave the corresponding localization not-done.
C#
public enum StringID
{
  Unit_UI_mm,
  Unit_UI_cm,
  Unit_UI_pt,
  Unit_UI_Inch,
}
I use the naming convention between the two enum. So I can check it by
C#
foreach(string unit_name in Enum.GetNames( typeof(Unit) ) )
{
  if( Enum.IsDefined( typeof(Unit), "Unit_UI_" + unit_name ) == false)
  {
    Debug.Assert(false, string.Format("need to add Localization for [{0}]", unit_name) );
  }
}

Using the code

To use the code, just add the EnumTester.cs file to your project, then re-compile your assembly and then throw it to NUnit to test.

C#
using System;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Collections;
using NUnit.Framework;

namespace EnumTester
{
    /// <summary>
    /// A NUnit class
    /// </summary>
    [TestFixture(Description="Test Enum definition: duplicated value, or error-prone value for [Flags]ed Enum such as 3")]
    public class EnumTest
    {
        [Test(Description="Test All the Enum types defined in this assembly")]
        public void Enum_Check_All()
        {
            Assembly assem = Assembly.GetExecutingAssembly();
            EnumTester_Common.check_assembly_enum_type( assem, EnumTest_Type.All );
        }
}

The basic idea is to detect all the Enum types in the Assembly using reflection, then check each Enum name definition and the values. If you known well about your Enum and simply want to Skip the test, just add the following Attribute to your Enum or to individual enum Item:

C#
[Skip_Enum_CheckAttribute]
public enum I_Know_My_Enum_Well_Even_Its_error_prone
{
  Name,
  name, //diff from the former only by case
  s0, //The digit 0
  sO, //The upper case character between 'N' and 'P'
  others = 0, //The value same as Name
}

public enum MyEnum
{
  Name,
  name, //diff from the former only by case
  [Skip_Enum_CheckAttribute]
  s0, //Check of s0 will be skipped, but checking of others still take it account into.
  sO, //The upper case character between 'N' and 'P'
  others = 0, //The value same as Name
}
Note the Attribute postfix for Skip_Enum_CheckAttribute is optional.

For the sake of refined control of the test, I include a Enum named

C#
EnumTest_Type
to define precisely which type of Test should be performed on the assembly. E.g., to detect value duplications, using
C#
[Test(Description="Test All the Enum types defined in this assembly")]
public void Enum_Check_All()
{
    Assembly assem = Assembly.GetExecutingAssembly();
    EnumTester_Common.check_assembly_enum_type( assem, EnumTest_Type.Duplicated_Value );
}

Points of Interest

Check constant name different only by case

C#
internal enum Enum_Diff_Only_Case
{
    abcdefghijklmnopqrstuvwxyz,

    ABCDEFGHIJKLMNOPQRSTUVWXYZ,
}

Check constan name different only by digit 0 and lower/upper character o

C#
internal enum Enum_Digit_0_Upper_Char_O
{
    Guess_digit_or_char_0,
    Guess_digit_or_char_O,
}

Check constant name different only by digit 1 and lower character l

C#
internal enum Enum_Digit_1_Lower_Char_l
{
    Guess_digit_or_char_1, //The decimal between 0 and 2
    Guess_digit_or_char_l, //the lower case of letter L
}

Tricks, Tips and pitfalls of Enum

There's several article on the CodeProject talks about how to operation Enum, especially by Reflection, here's the partial collection of the tricks and tips, and of my own:

It's possible to add a comma after the last item in enum definition.

I think it's very convenient compared to C's rule:

C#
int ia[] = {1, 2, 3, 4,  };
The last comma only allowed in the above construct, not in enum. This little improvement did make sense for enum, since mostly you want to re-group the enum items.

value for the item derived from it's latest previous sibling

C#
public enum WeekDay
{
  SunDay ,       //The first default value always 0
  MonDay = 2,    //You can explicitly specify a value
  TuesDay = 2,   //Even it's duplicated
  Wednesday ,    //Wensday will be 3, determined by Tuesday's value
  Thursday ,     //will be 4, determined by Wednesday
  Friday = 100,  //Ok
  Saturday,      //Will be 101
}
The above commented rules is same as C/C++.

Enum.GetNames( Type enumType) will return the names in the order of definition

Enum.GetName(Type enumType, object value) will also return first item in the above order in case of more than one item defined the same value

C#
public enum EnumTest: int
{
  None = 0, 
  One  = 0,
}

Console.WriteLine( Enum.GetName(typeof(EnumTest), (int)0 ) ); //Get None

public enum EnumTest: int
{
  One  = 0,
  None = 0, 
}

Console.WriteLine( Enum.GetName(typeof(EnumTest), (int)0 ) ); //Get One

0 is special Enum value

You can assign 0 to a Enum variable directly, other value not allowed:
C#
MyEnum val = 0;
val = 1; //Must cast

When cast is needed, always use (MyEnum) syntax, GetObject() as MyEnum not allowed

Enum is value type. "as" only applies to reference type. This is a general rule for all the value types, not limited to Enum.

Define the underlying type of enum

C#
public enum MyColors: long
{
    ...,
}
int is the default at least for C#, it's ok in the most case. I want to repeat myself here that char is *NOT* the legal underlying type for enum.

Get the underlying type of enum, and Unmanaged sizeof of enum

C#
Type underlying_type = MyEnum.GetUnderlyingType();

int unmanaged_size = System.Runtime.InteropServices.Marshal.SizeOf(underlying_type);

//The following line will occur an exception
int unmanaged_size = System.Runtime.InteropServices.Marshal.SizeOf( MyEnum );

C# compiler won't give any warning about an error-prone [Flags] Enum value even csc /w:4 is specified

C#
[Flags]
public enum MyEnum
{
  First  = 0x1;
  Second = 0x3;
}

//Combine MyEnum.First | MyEnum.Second will result in the weird result in your code.
//This article will tackle this issue by auto-detect such a case.

Commandments from [Effective C#](Item 8): Make 0 a valid value for enum

Simply saying a value type will be initialized to 0 by default. For more discuss please refer the book.

Get all the defined Enum constants:

C#
string[] all_names = Enum.GetNames( typeof(MyEnum) );
or, there's harder way to do the same thing:
C#
ArrayList all_names = new ArrayList();
Type t = typeof(MyEnum);
FieldInfo[] fields = t.GetFields( BindingFlags.Public | BindingFlags.Static);
foreach(FieldInfo f in fields)
{
   all_names.Add( f.Name );
}

To detect whether a given type is a Enum type

C#
Type t = null; //From Reflection
if( t.BaseType == typeof(Enum) ) {...}

The legal underlying types for Enum:

error CS1008: Type byte, sbyte, short, ushort, int, uint, long, or ulong expected

I get it by try-error, so you needn't. Note that char is *NOT* a valid underlying type for Enum.

How to get an Enum variable's instance by it's Name?

C#
enum_val = (MyEnum_WeekDay) TypeDescriptor.GetConverter(enum_val).ConvertFrom("Sunday");
I customed to the following way for a long time, but don't know the above way, fortunatly, suggested by Dactyl (thank you!), the later is 3-times faster than using the TypeConverter class.
C#
Enum.Parse( typeof(MyEnum_WeekDay), "Sunday") ;

How to get an Enum variable's instance by a int value?

If you can access the Enum type at compile time, that's easier:
C#
MyEnum val = (MyEnum)300;
Console.WriteLine( ((int)val).ToString() ); //Will output 300 if 300 is not a defined value
No exception will be thrown even 300 is not a defined value. It's harder when not knowing the compile time Enum type:
C#
Convert.ChangeType( (int)300, runtime_enum_type );
But this will result in the following exception:
C#
System.InvalidCastException: Invalid cast from System.Int32 to Client+TestEnum.
Even the underlying type of runtime_enum_type is exactly int. Note it's ok in the reverse conversion:
C#
Convert.ChangeType( enum_val, typeof(long) );
Even that long is not the underlying type of the Enum. Maybe there's an easy way and I just miss it(tell me if there's), here's my own:
C#
Type underlying_type = t.GetUnderlyingType();
string enum_name =
  Enum.GetName(t, Convert.ChangeType( int_value, underlying_type ) );

Enum.Parse(t, enum_name );
Note that change the raw decimal number to the real underlying type is a must, otherwise the following exception will be thrown:
C#
System.ArgumentException: Enum underlying type and the object must be same type or object
must be a String.  Type passed in was System.Int64; the enum underlying type was System.In
t16.
The same rule also apply to the following method:
C#
static Enum.IsDefined( Type enum_type, object obj)
The valid type of the obj is either string or the exact underlying type of the Enum. Of course for the string it's should be the Enum Constant. The above snippet can be used to detect whethter a given string or int/short etc is a defined item in the Enum.

History

This is my first post on CodeProject. Hope this will be useful to you.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Software Developer
China China
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --