6

I've written the class SomeObject and I want to define a const instance of this object to keep/reuse in my TestCases. How should I rewrite the code below to achieve this behavior?

[TestFixture]
public class SomeObjectTests
{
    private const SomeObject item0 = new SomeObject(0.0); // doesn't work

    [TestCase(item0, ExpectedResult = 0.0)]
    public double TestSomeObjectValue(SomeObject so)
    {
        return so.Value;
    }

    [TestCase(item0, ExpectedResult = "0.0")]
    public string TestSomeObjectValueString(SomeObject so)
    {
        return so.ValueString;
    }
}

I get the following error message:

A const field of a reference type other than string can only be initialized with null.

budi
  • 6,351
  • 10
  • 55
  • 80
  • remove `const` from your code, it is something that is just static, and it isn't going to remain constant. – Habib Dec 03 '15 at 20:46
  • 4
    use `static readonly` instead of `const`. c# does not support const on all types. the type must be compile time constant. see here http://stackoverflow.com/questions/6231253/how-to-declare-a-class-instance-as-a-constant-in-c – M.kazem Akhgary Dec 03 '15 at 20:48
  • EDIT: Added the error message I am getting. Is my problem with regards to NUnit, or am I missing something fundamental from C#? – budi Dec 03 '15 at 20:49
  • 1
    It's something fundamental in C#. object references cannot be compile-time constant, so are therefore ineligible for `const`. `string` is an exception, however. – Wai Ha Lee Dec 03 '15 at 20:51
  • 1
    @M.kazemAkhgary You can use `const` with *any* type. Every single type has at least one compile time constant literal of that type. Most types can't create any *useful* `const` value because most meaningful values of that type have no compile time literal. – Servy Dec 03 '15 at 20:53
  • @WaiHaLee You explain in your own comment why your comment is wrong. There is no fundimental rule that object references cannot be constants. The rule is that all constants must be compile time literals; it's just that the only compile time literal object references that you can create are strings. – Servy Dec 03 '15 at 20:54

3 Answers3

9

A better way to achieve what you are trying to do is to use TestCaseSource. In your case:

[TestFixture]
public class SomeObjectTests
{
    [TestCaseSource(typeof(TestSomeObject),"TestCasesValue")]
    public double TestSomeObjectValue(SomeObject so)
    {
        return so.Value;
    }

    [TestCaseSource(typeof(TestSomeObject),"TestCasesValueString")]
    public string TestSomeObjectValueString(SomeObject so)
    {
        return so.ValueString;
    }
}

public class TestSomeObject
{
  public static IEnumerable TestCasesValue
  {
    get
    {
        yield return new TestCaseData( new SomeObject(0.0) ).Returns( 0.0 );
        yield return new TestCaseData( new SomeObject(1.0) ).Returns( 1.0 );
    }
  }

  public static IEnumerable TestCasesValueString
  {
    get
    {
        yield return new TestCaseData( new SomeObject(0.0) ).Returns( "0.0" );
        yield return new TestCaseData( new SomeObject(1.0) ).Returns( "1.0" );
    }
  }
}
Daniel Rose
  • 17,233
  • 9
  • 65
  • 88
  • TestCaseData is a way to describe the data, as well as what you want to do with the data. Normally, you could return SomeObject directly, but since you use the return value of the test method, you can use TestCaseData to also describe the expected return. – Daniel Rose Dec 03 '15 at 21:53
5

The C# language spec, §10.3 says (emphasis mine):

When a symbolic name for a constant value is desired, but when the type of that value is not permitted in a constant declaration, or when the value cannot be computed at compile-time by a constant-expression, a readonly field (Section 10.4.2) may be used instead.


Annoyingly, this is compounded by the fact that attributes have certain restrictions too - see the C# language spec, §17.2 (again, emphasis mine):

An expression E is an attribute-argument-expression if all of the following statements are true:

  • The type of E is an attribute parameter type (Section 17.1.3).

  • At compile-time, the value of E can be resolved to one of the following:

    • A constant value.

    • A System.Type object.

    • A one-dimensional array of attribute-argument-expressions.

Where §17.1.3: "Attribute parameter types" says1:

The types of positional and named parameters for an attribute class are limited to the attribute parameter types, which are:

  • One of the following types: bool, byte, char, double, float, int, long, short, string.
  • The type object.
  • The type System.Type.
  • An enum type, provided it has public accessibility and the types in which it is nested (if any) also have public accessibility (§17.2).
  • Single-dimensional arrays of the above types.

1: the quoted text is from an older version of the C# specification - in the C# 5.0 version, four additional types are mentioned: sbyte, uint, ulong, and ushort.


In other words, the best you can do is something like:

[TestFixture]
public class SomeObjectTests {

    private static readonly SomeObject item0 = new SomeObject(0.0);

    private static SomeObject getObject(string key) {
        if ( key == "item0" )
            return item0;

        throw new ArgumentException("Unknown key");
    }

    [TestCase("item0", ExpectedResult = 0.0)]
    public double TestSomeObjectValue(string key) {
        SomeObject so = getObject(key);
        return so.Value;
    }

    [TestCase("item0", ExpectedResult = "0.0")]
    public string TestSomeObjectValueString(string key) {
        SomeObject so = getObject(key);
        return so.ValueString;
    }
}

This way, the arguments to the attributes are compile-time constant, and the getObject method can handle getting the SomeObject instance.

Wai Ha Lee
  • 8,598
  • 83
  • 57
  • 92
  • Ok, so it looks like I'm having an NUnit problem then. I get the error message: `An attribute argument must be a constant expression, typeof expression or array creation expression of an attribute parameter type` – budi Dec 03 '15 at 21:07
  • Hmm - I've only *half* answered your question. Give me a moment to update my answer (I'm on my phone). – Wai Ha Lee Dec 03 '15 at 21:10
  • Seems counter-intuitive to have to create a factory just for something as simple as what I'm trying to accomplish, after doing some googling myself it appears to be the best solution. Cheers. – budi Dec 03 '15 at 21:30
  • No problem. It is a bit horrible to have to do that, but that's the way C# is unfortunately. Glad to gave helped you! – Wai Ha Lee Dec 03 '15 at 21:33
  • C# wants attribute data to be a compile-time constant (which can be annoying sometimes, as you notice). Thus, NUnit also allows specifying the test cases via TestCaseSource, for when you can't specify the values via a compile-time constant. – Daniel Rose Dec 03 '15 at 21:39
  • The quoted statement is false. You can create a `const` identifier of any type, including user defined classes, structs, arrays, etc. You are simply limited to assigning compile time constant values to them, and for all of those classes there is usually no meaningful compile time constants that you would ever want to assign to them. – Servy Dec 03 '15 at 22:02
  • @Servy - is it that I was mis-quoting it, or was it just wrong? In either case, I've quoted from the C# language spec instead, which (I think) clarifies the point you're trying to make - only `string` can have a non-`null`, `const` value. – Wai Ha Lee Dec 03 '15 at 22:44
  • ... how could I declare a `const` struct? I cannot use `new` and it cannot be `null`. – Wai Ha Lee Dec 03 '15 at 23:00
  • 1
    @WaiHaLee You can create a `const` identifier of any valid C# type (either value or reference types) using the compile time literal `default(TheTypeOfVariableGoesHere)`. You can also use `typeof` to create compile time constant values of type `Type`. After re-reading it, I see that the quote is in fact technically correct, my mistake, but your conclusions about it are not. Even though you can't create sensible values of most types, you can still create `const` identifiers of those types and have at least one valid value to assign to it. – Servy Dec 03 '15 at 23:31
  • @Servy - of course. I had forgotten that `default` is compile-time constant. I've edited my question to try to resolve any ambiguities. – Wai Ha Lee Dec 04 '15 at 00:09
  • @Servy is wrong if he says the `const` keyword can be used with any type. For example `const TimeSpan myZeroSpan = default(TimeSpan);` is not allowed. The specification does not allow it, and I do not expect any implementations of the C# compiler to allow it. I am only talking the `const` keyword here. I am not talking about which expressions are allowed in attribute applications, that is not quite the same. Being a value the compile can know at compile-time is not the same as being a value that can be declared `const` then. – Jeppe Stig Nielsen Dec 25 '15 at 16:04
  • 1
    @JeppeStigNielsen - on further investigation, you are right. The language spec [§10.3](https://msdn.microsoft.com/en-us/library/aa645749(v=vs.71).aspx) (10.4 in C# 5.0) says: "*The type specified in a constant declaration must be `sbyte`, `byte`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`, `char`, `float`, `double`, `decimal`, `bool`, `string`, an `enum`-type, or a reference-type. Each constant-expression must yield a value of the target type or of a type that can be converted to the target type by an implicit conversion*" – Wai Ha Lee Dec 26 '15 at 16:41
  • 1
    Likewise, [§17.1.3: "*Attribute parameter types*"](https://msdn.microsoft.com/en-us/library/aa664615(v=vs.71)) says: "*The types of positional and named parameters for an attribute class are limited to the attribute parameter types, which are: (a) one of the following types: `bool`, `byte`, `char`, `double`, `float`, `int`, `long`, `short`, `string`, (b) the type `object`, (c) the type `System.Type`, (d) an enum type, provided it has public accessibility and the types in which it is nested (if any) also have public accessibility (§17.2), (e) single-dimensional arrays of the above types.*". – Wai Ha Lee Dec 26 '15 at 16:49
  • In other words, my answer, as it is *at the moment*, is inaccurate. – Wai Ha Lee Dec 26 '15 at 16:51
  • Sure, and the first list of allowed `const` types was what I wrote in my old comment to [an answer by Chris S in another thread](http://stackoverflow.com/a/755705/1336654) that you saw originally. – Jeppe Stig Nielsen Dec 26 '15 at 16:51
  • @JeppeStigNielsen - thanks for your comments. I've edited my answer accordingly - it was bothering me. – Wai Ha Lee Dec 26 '15 at 17:23
0
  1. Just remove the const. It will be a private variable for each instance
  2. Make it a static and it will be a singleton for the class.
  3. Replace const with readonly. That will flag this as something that shouldn't be messed with.
wandercoder
  • 392
  • 2
  • 16