Метод должен вызывать исключение, но это не так

#c# #unit-testing #mstest

#c# #модульное тестирование #мстест

Вопрос:

Я написал небольшой метод расширения, который находит индексы данной строки в любом IEnumerable.

 public static IEnumerable<int> FindIndexesOf(this IEnumerable<string> itemList, string indexesToFind)
{
    if (itemList == null)
        throw new ArgumentNullException("itemList");
    if (indexesToFind == null)
        throw new ArgumentNullException("indexToFind");

    List<string> enumerable = itemList as List<string> ?? itemList.ToList();
    for (int i = 0; i < enumerable.Count(); i  )
    {
        if (enumerable[i] == indexesToFind)
            yield return i;
    }
}
 

Как вы можете видеть выше, исключение ArgumentNullException выдается, если ItemList имеет значение null. Просто и ясно.

При запуске моего unittest в приведенном выше методе я ожидаю и исключение типа ArgumentNullException, потому что ItemList имеет значение null . Однако тест выдает false, потому что исключение не генерируется.

Как это возможно? Логика кажется довольно ясной. Смотрите тест ниже.

 [TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void FindIndexesOfTest2()
{
    string[] items = null;
    IEnumerable<int> indexes = items.FindIndexesOf("one");
}
 

Где я ошибаюсь в своей логике; почему он не выдает исключение ArgumentNullException?

Комментарии:

1. Вы запускали код в отладчике и проверяли, действительно ли параметр равен null?

2. У меня есть, да. На самом деле это null .

Ответ №1:

Проблема в том, что использование счетчиков yield оценивается лениво.

Поскольку вы не выполняете итерацию по возвращенной коллекции, метод фактически не выполнен.

Правильный способ сделать это — разделить метод на две части:

 public static IEnumerable<int> FindIndexesOf(this IEnumerable<string> itemList, string indexesToFind)
{
    if (itemList == null)
        throw new ArgumentNullException("itemList");
    if (indexesToFind == null)
        throw new ArgumentNullException("indexToFind");

    return FindIndexesOfImpl(itemList, indexesToFind);    
}

private static IEnumerable<int> FindIndexesOfImpl(this IEnumerable<string> itemList, string indexesToFind)
{
    List<string> enumerable = itemList as List<string> ?? itemList.ToList();
    for (int i = 0; i < enumerable.Count(); i  )
    {
        if (enumerable[i] == indexesToFind)
            yield return i;
    }
}
 

Здесь первый метод будет выполняться при его вызове и возвращать лениво вычисляемый перечислитель, который этого не сделал, пока вы не выполните итерацию по нему.

Хотя я бы посоветовал вам также изменить последний метод здесь, чтобы он был действительно лениво оценен. Тот факт, что метод кэширует все itemList только для того, чтобы иметь возможность использовать индексы, не нужен, и вы можете фактически переписать его без него:

 public static IEnumerable<int> FindIndexesOfImpl(this IEnumerable<string> itemList, string indexesToFind)
{
    var index = 0;
    foreach (var item in itemList)
    {
        if (item == indexesToFind)
            yield return index;
        index  ;
    }
}
 

Вы также можете использовать методы расширения LINQ для этого, хотя это включает в себя создание временного объекта для каждого элемента, не уверенный, стоит ли оно того, я бы выбрал тот, который чуть выше здесь:

 public static IEnumerable<int> FindIndexesOfImpl(this IEnumerable<string> itemList, string indexesToFind)
{
    return itemList
        .Select((item, index) => new { item, index })
        .Where(element => element.item == indexesToFind)
        .Select(element => element.index);
}
 

С помощью этого последнего метода вы можете переместить его обратно в основной метод, потому что вы больше не используете yield :

 public static IEnumerable<int> FindIndexesOf(this IEnumerable<string> itemList, string indexesToFind)
{
    if (itemList == null)
        throw new ArgumentNullException("itemList");
    if (indexesToFind == null)
        throw new ArgumentNullException("indexToFind");

    return itemList
        .Select((item, index) => new { item, index })
        .Where(element => element.item == indexesToFind)
        .Select(element => element.index);
}
 

Комментарии:

1. Работает, ты лучший!

2. Обратите внимание, что на самом деле может быть лучше просто создать список в методе и вернуть его, содержащий результаты, вместо того, чтобы использовать здесь перечислитель, поскольку вы на самом деле itemList все равно кэшируете все. Есть другой способ написать этот последний метод, хотя и без необходимости использования кэша, я также опубликую его.

3. Какой из этих двух вы бы порекомендовали?

4. Я бы рекомендовал либо тот, который использует foreach и index переменную, либо последний, который возвращается только к 1 методу.

5. Да, но какой из них? Каковы преимущества для них обоих?