Nunit: Request: Add support of indexers to the PropertyConstraint

Created on 2 Jun 2017  ·  46Comments  ·  Source: nunit/nunit

At the moment it is only possible to test values of normal properties and no way to test indexer values. I need to write a test that looks something like this:

```c#
[TestFixture]
public class TestClass
{
class Example
{
private Dictionary dictionary = new Dictionary();

  public string this[string key]
  {
    get { return dictionary[key]; }
    set { dictionary[key] = value; }
  }
}

[Test]
public void Test()
{
  var obj = new Example
  {
    ["TEST1"] = "1",
    ["TEST2"] = "2",
  };

  Assert.That(obj, Has.Property("Item", "TEST1").EqualTo("1").And.Property("Item", "TEST2").EqualTo("2"));
}

}
```

done help wanted enhancement normal

Most helpful comment

And as for pay grade, since everyone here is making an annual salary of about $0 working on NUnit, I think we're all at the same pay grade 😝

All 46 comments

I like the idea.

C# doesn't support named indexed properties but VB.NET does. I think the syntax .Property("Name", index1, ...) should be reserved for named indexers, so I wouldn't want to see that be used for a default indexer. I'd like to be able to respect the C# language by not requiring people to pass in a name parameter.

What do you think about Has.Item("TEST1").EqualTo("1") or Has.Index("TEST1").EqualTo("1")?

@jnm2
Having "Item" or "Indexer" method as a shortcut is convinient, but it is actually possible to change indexer name in C#: https://stackoverflow.com/questions/5110403/class-with-indexer-and-property-named-item

@Ch0senOne It is possible to change the name. To be clearer I should have said, it is not possible to make a non-default indexer in C#. For example, you can't declare two indexers, nor can you consume the name from C#.
Thinking it over I'm afraid Has.Item("value") would be too confusable with Contains.Item. So maybe Has.Index("value")?

So I'd be in favor of Has.Index("IndexValue") for the default indexer and Has.Property("PropertyName", indexParams) for non-default indexers. Has.Property would end up working for default indexers too even though the idiomatic C# thing to do would be to use Has.Index.


Or if we wanted to be cute, we could do Has.Index["IndexValue"] and
Has.Property("PropertyName")[indexparams]... 😁

Actually, I wonder if we could get away with Has.Item["AtIndex"] using indexer syntax in order to avoid being confused with Contains.Item("ItemValue")... not sure what I think about that!


Let's see what the other @nunit/framework-team members think.

We already defined a direction to go on properties using lambdas rather than strings. Seems like time might be better spent on that. I'm on my phone so I can't give you the issue number.

@CharliePoole https://github.com/nunit/nunit/issues/26?

That makes sense for Has.Property. We could do:

  • Has.Property(_ => _.PropertyName[index1, ...])
  • Has.Property(_ => _.PropertyName, index1, ...)

I still think we should not do anything with Has.Property until non-default properties are needed, and for now just provide Has.Index(value1, ...) or Has.Index[value1, ...] or Has.Item[value1, ...] for the vastly more common default indexers.

That's the one. I agree with you on named indexers, which haven't been requested anyway.

@jnm2 For your consideration, you can make a multi-keyed indexer as well. For example

this[string key1, string key2]

I'm wondering if there is any update on this issue, or current way of testing indexers on a type that doesn't inherit IEnumerable. I suppose I could just check for a KeyNotFoundException being thrown. Anything else I'm overlooking to allow me to check for the existence of a key in an indexer?

@crush83 Yes, my index1, ... syntax was meant to indicate that we should consider them whatever we do.

I do not like the terminology key unless we are talking about an object that is actually a dictionary or keyed collection. Those are things with default indexers, but not all things with default indexers have keys. (Since you are using this terminology, maybe you'd find our Contains.Key syntax to be helpful? Can we improve it?)

There's been no progress other than what you see here. We need a proposal. To put something out there, my current favorite is Has.Item(params object[] indexerArgs) which would call the default indexer no matter what its name is.
Would this do everything requested so far?

+1

@nunit/framework-team Would you support the following new API?

namespace NUnit.Framework
{
    public abstract class Has
    {
        public static ResolvableConstraintExpression Property(string name);

+       public static ResolvableConstraintExpression Item(params object[] indexerArgs);
    }
}

namespace NUnit.Framework.Constraints
{
    public class ConstraintExpression
    {
        public ResolvableConstraintExpression Property(string name);

+       public ResolvableConstraintExpression Item(params object[] indexerArgs);
    }
}

I would probably leave the IndexerConstraint class internal until there's a reason to add it to the API.

Sounds good to me.

Another vote in favour, I just wanted this feature for a console test. 🙂

@jnm2 Is this issue still something the team wants done?

I had a quick squiz through the issue and as I don't understand exactly want the supported API should look like - is it:

/// obj from the initial post example
Assert.That(obj, Has.Item("TEST1").EqualTo(1).And.Item("TEST2").EqualTo(2));

or is it:

Assert.That(obj, Has.Item("TEST1", 1).And.Item("TEST2", 2));

?

You example takes params object[] so was going with option 2.

Assuming opt. 2, would the array be required as if just one object provided, assert that the key exists, 2 parameters - key and value? Other combinations (more or less expected parameters), fail?

@Poimen Yes, and we'd appreciate the help!

The array is required because indexers can take more than one argument. For example:

public string this[int x] => "First indexer";

public string this[string x] => "Second indexer";

public string this[int x, int y] => "Third indexer";

You'd use Has.Item(42).EqualTo("First indexer"), Has.Item("42").EqualTo("Second indexer"), and Has.Item(1, 2).EqualTo("Third indexer").

See, you learn something new every day - I had assumed that was a typo, oops :see_no_evil: I have not had the pleasure of using a multi-key indexer before...

Ok, cool, I'll look at putting something together this week.

@jnm2 I have created a PR for this issue.

The one concern that I have is the error message that is produced by the code:

Has.Item(42)

The error message is a little "hacky", but I'm not sure what the cleanest way of resolving this. The "hack" is:
https://github.com/nunit/nunit/pull/3609/commits/96154ca6402c692cec5e0ae6947f9922e72799fe#diff-ceeee4b8399633f181b6bccd5a39eb32R72

@Poimen Sorry, not sure what you mean. What's the message currently, and what would you like it to be? Or are you looking for suggestions?

@jnm2 The error message that appears is:

  Expected: indexer [System.Int32]
  But was:  "not found"

The way the error messages are formed StringifyArguments function is a little "hacky".

So my badly expressed question is more if there was a cleaner way of producing the desired message output.

@jnm2 I think I have addressed the comments on the review (thanks for them). The label says awaiting:contributor.

I'm not sure if there is something more I need to do to let you know that I'm "done" :+1:

I think we should make it as close as possible to the message that is shown when Has.Property can't find a property. We should then replace property with default indexer and replace the property name with accepting arguments [42] or something like that. I lean towards thinking we should use one of the existing formatting helpers like MsgUtils.FormatCollection to show the argument values rather than listing the argument types. For example, one of the arguments might be null and work with a range of types.

Nope, that's great! A comment just like that in the PR thread itself would be ideal.

Given the assertion:
```c#
Assert.That(tester, Has.Item("wow"));


It producers the message:

Expected: Default indexer accepting arguments < >
But was: "not found"


Given the assertion:
```c#
Assert.That(tester, Has.Item("wow").EqualTo(1));

It producers the messages:

Default indexer accepting arguments < "wow" > was not found on NUnit.Framework.Constraints.IndexerConstraintTests+NamedIndexTester.

This generated using the MsgUtils.FormatCollection function.

So also more closely matches the Property messages, with the added details.

I like the second case very much. The first case still seems semantically wrong to me because it shows argument types instead of argument values. We only have argument values, and each argument value can match a range of argument types in the indexer. It's misleading to make it look like there must be an indexer with a System.String argument because an IEnumerable<char> argument would work just as well.

IEnumerable<char> is a wacky use case...but sure, I get the point.

So, back to - given:
```c#
Assert.That(tester, Has.Item("wow"));


producers the message:

Expected: Default indexer accepting arguments < "wow" >
But was: "not found"


For numbers it goes into the usual suffix - given:
```c#
Assert.That(tester, Has.Item(42d));

producers:

  Expected: Default indexer accepting arguments < 42.0d >
  But was:  "not found"

The 'Expected' message looks great then to me. The But was: "not found" message looks a little like it's talking about a string instance with the contents not found, but if PropertyConstraint behaves the same way, keeping them the same is preferable to me. Changing PropertyConstraint would be out of scope for this PR by default. We could still make a change if you wanted to and I and someone else in the framework team okayed the change.

Yeah, I was trying to make it not do this, but I couldn't find a solution with the error message. I would have liked it just to be:

But was: not found

but the return of ConstraintResult is putting the string quotes in there.

For reference the property version:
```c#
Assert.That(tester, Has.Property("NoItDoesNotHaveThis"));

produces:

Expected: property NoItDoesNotHaveThis
But was:
```
So there are no quotes - but it returns the type - not a value.

I wasn't sure if returning an overload of ConstraintResult would be a solution?

I think we should stick with what PropertyConstraint is doing for consistency. Showing the default representation of what the actual value is isn't the worst thing when the message saying that what failed was finding the property.

Returning a new class derived from ConstraintResult is how other constraints customize this, yes.

Ok, great - sounds like - given:
```c#
Assert.That(tester, Has.Item(42d));


producers:

Expected: Default indexer accepting arguments < 42.0d >
But was:
```

This matches the property constraint message.

@nunit/framework-team and everyone, I'm hoping for a speedy resolution on this question since @Poimen's excellent PR (https://github.com/nunit/nunit/pull/3609) is ready to merge otherwise.

@Dreamescaper brought to my attention my concern higher up in this thread that I lost track of: I'm afraid people will both write and read Assert.That(collection, Has.Item("x")); thinking that Has.Item means Contains.Item.
We could consider adding something to the failure message, but that won't help the problem of understanding it properly when reading Has.Item in source code:

  Expected: Default indexer accepting arguments < 42.0d > (Did you mean to use Contains.Item?)

On the other hand, I don't like Has.Index(42) or Has.Index(42).EqualTo(43) which is what I had suggested instead of Item. The instance has an item at an index. You wouldn't say the instance has an index and that the index itself is equal to 43. The index is thing you pass in, 42.

Some of the options

  • Use Has.Item. Give up on the problems it might cause when reading source code. Maybe add some mitigations to help when writing source code, like XML docs and tips in failure messages.
  • Use Has.Index despite the awkward mixing of terminology.
  • Use Has.ItemAt instead. Has.ItemAt(42).EqualTo(43).

Hold on, I just fell in love with the last suggestion since it has a parallel with the LINQ ElementAt method which is given an index and returns an item.

Is everyone happy with Has.ItemAt? Suggestions welcome but again I'd like to be quick to respect the time that @Poimen has dedicated already.

+1 for Has.ItemAt :)

I agree with you that Has.Item is potentially confusing. I like Has.ItemAt but since you like the LINQ syntax, why not use Has.ElementAt ?

I'm not sure about ElementAt, as it's not a direct equivalent.
E.g. for Dictionary<string,string> LINQ's ElementAt(0) will return the first pair, and Has.ItemTo(0).EqualTo("...") will fail, as there is no indexer accepting integers.

@CharliePoole That's a really good thought. It seems like it will not be as intuitive to find, but I don't know why I think that. I think I prefer Item because we will call an indexer on presumably a collection/dictionary/lookup of some kind, whereas ElementAt is intended to work in queries and other enumerables that aren't necessarily collections. Collection elements are usually called items.

@Dreamescaper Do you think Has.ItemAt(4) could make people think it works just like ElementAt, enumerating rather than calling an indexer?

@jnm2
It probably could, but I don't know a better option (and I think it's still a much better option than Has.Item).

@jnm2 I'm actually a little confused. This issue started out being about Properties and is still so titled. How will Has.Property and Has.ItemAt relate or interact or compare with one another in the eyes of the user?

[I realize that properties and collection Items can be treated as equivalent at a certain level of abstraction, but I don't think it's the level of abstraction at which most of us usually operate. :smiling_imp: ]

As an NUnit 4.0 thing, it might be nice to go through how terms are used in the API and clean up a bit. The word "member" is one that's often used as equivalent to "Item" but might also mean property, method or variable.

I feel most comfortable with ItemAt over ElementAt partly because it might look like we delegate to or take the same particular approach as Enumerable.ElementAt. What struck me strongly with ElementAt(int index) was less the specifics of how it worked and more the fact that there's precedent for an At suffixes in the BCL, especially associated with a parameter named index.

If you use this with an ILookup, each index of an ILookup returns multiple TElements, not a single element/item. Has.EntryAt could be something that works with collections, dictionaries, and multiple-element-per-index lookups. My initial reaction is that Has.ItemAt that returns IEnumerable<TElement> from an ILookup is probably understandable enough.

What do you all think? It's hard when you can see more than one thing working, but the majority preference here might also help indicate which name would be the most easy for people to find. People besides @nunit/framework-team, I didn't mean to exclude you. If you're watching and you have a strongish feeling, that's a data point that is always helpful!

@CharliePoole Has.Property("X") asserts that the instance has a non-parameterized property called X. Has.ItemAt("X") would assert that the instance has a default indexer (a parameterized property with an irrelevant, well-known name) that can accept a string parameter.

Has.Property("X").EqualTo(3) asserts that the instance has a non-parameterized property called X that returns 3 if you call its non-parameterized get accessor. Has.ItemAt("X").EqualTo(3) would assert that the instance has a default indexer (a parameterized property with an irrelevant, well-known name) that returns 3 if you pass the specific string "X" to its get accessor.

What makes the name of the default indexer irrelevant is that C#, VB and F# syntax can all call default indexers without specifying a name. (Thus the term 'default.') A language that had no concept of default indexers but which had a concept of named indexers would have to specify the name. The name of the default indexer is usually Item, but the default indexer can be named something arbitrary and still be the default indexer because those languages would still be able to call it without specifying a name. In C#, you'd accomplish this using the [IndexerName("ArbitraryName")] attribute.

@jnm2 I actually understand all the C# etc. language stuff about indexers... however...

I think there's potential for confusion between the property

  • __being__ an indexer of the actual object
  • __returning__ an object that has an indexer
  • __returning__ a collection with items in it

I stress that __you__ won't be confused and I'll probably only pretend to be when talking with you (socratic method, y' know :wink: ) but I suspect many may find it confusing and you'll be explaining it to folks quite a lot.

I'm not against the addition but I think the distinctions above would need to be spelled out in the docs.

PS: I didn't feel excluded at all. :smiley:

@CharliePoole Good thought. Can we table that consideration until we settle on a name, since it sounds like it won't affect the choice between Has.ItemAt/Has.ElementAt/Has.EntryAt/etc? Or is there a point I missed and you are supporting a particular naming choice based on this potential for confusion?

So far I'm leaning towards ItemAt. @Dreamescaper, is that still where you are too? @CharliePoole I wasn't sure if you were expressing a preference for ElementAt or just helping us think through the process.

The more people chime in on Has.ItemAt/Has.ElementAt/Has.EntryAt/other, the more confidence we can have in what people will find intuitive. I'd like to just call it for whichever seems to be in the lead in the next day or so, since we will miss the 3.13 release by such a tiny amount if we take much longer than that.

@jnm2 Just trying to think it through myself and encourage the same.

WRT the choice of words, I was wondering whether some particular wording will work best with the three types of expressions I listed and also whether there are any other combinations to worry about. With the three options I mentioned, you would have

  • Has.ItemAt("foo").EqualTo("bar")
  • `Has.Property("Bing").With.ItemAt("foo").EqualTo("bar")
  • `Has.Property("Bing").With.One.EqualTo("bar")

So I guess you're right that the wording doesn't matter for any options

Looking toward the longer term, I'd rather use actual indexing with square brackets. But that would require actually compiling the constraint expression rather than modeling as a set of classes.

I know that this is above-my-pay-grade :smile:, but thought I'd chime in here...

I take a liking to ItemAt.

I spent time, as @CharliePoole suggested, thinking through some use cases. I came to the thought that ElementAt is used more in IEnumerable situations and the language indexers can handle more than just IEnumerable. I sat on the ElementAt fence for awhile, then considered:
```c#
Assert.That(..., Has.ElementAt("one", "two"));

against:
```c#
Assert.That(..., Has.ItemAt("one", "two"));

ItemAt in the above feels more natural to express multiple value indexers.

I also considered that the ticket originally intended for a short-circuit for a property constraint and ItemAt fits with that narrative. (However, this doesn't preclude it to morph into something more/better/etc, just a point-of-view).

I also prefer ItemAt, so let's call it a majority and rule in favor of that 😺

To prevent confusion, let's make sure that this gets documented quickly after completion and included in release notes.

@Poimen Re: your pay grade, do not underestimate the extent to which we're all making things up as we go along here. Thanks for sharing your thoughts!

And as for pay grade, since everyone here is making an annual salary of about $0 working on NUnit, I think we're all at the same pay grade 😝

@Dreamescaper asked another good question in the PR. Whether positive or negative, this does like it would be checking the count rather than checking for the presence of an indexer:

Assert.That(new[] { 1, 2, 2 }, Has.ItemAt(3));
Assert.That(new[] { 1, 2, 2 }, Has.No.ItemAt(3));

It seems safest to remove the Exists constraint until we have more time to think about it. I'd prefer opening a separate issue for Has.Indexer(typeof(Type1), typeof(Type2), ...) for an indexer existence check, and Has.ItemAt(arg1, arg2, ...) to actually run the indexer and assert things about the resulting value.

Would anyone object to initially shipping Has.ItemAt(3) as not resolving to a constraint and Has.ItemAt(3).EqualTo(4) as resolving?

Has.Indexer makes sense to me. I assume you would also support Has.Indexer<T>(), Has.Indexer<T1, T2>(), etc. for consistency with other constraints.

Thanks for the feedback! I gave the go-ahead on the pull request to make Has.ItemAt(...) no longer be self-resolving, and I filed https://github.com/nunit/nunit/issues/3690 to track Has.Indexer to provide that capability.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

JackUkleja picture JackUkleja  ·  5Comments

Thaina picture Thaina  ·  5Comments

ffMathy picture ffMathy  ·  3Comments

TobiasSekan picture TobiasSekan  ·  4Comments

jnm2 picture jnm2  ·  4Comments