Spock:从CSV文件中读取测试数据

时间:2014-08-07 18:15:04

标签: unit-testing csv groovy spock opencsv

我正在尝试编写一个优雅的Spock规范,该规范将从CSV文件中读取非常大的测试数据,而不会将所有数据加载到内存中。我正在寻找你的反馈,你可能会比我现在拥有的更好。

我们假设我的简化CSV文件如下所示: -

1,2
3,4
5,6

断言是"column 1" + 1 == "column 2"

我正在使用OpenCSV进行CSV解析,因为实际的CSV文件包含带有双引号和逗号等特殊字符的字符串,以及通过逗号分割字符串进行基本解析等等。

<dependency>
    <groupId>net.sf.opencsv</groupId>
    <artifactId>opencsv</artifactId>
    <version>2.3</version>
</dependency>

尝试1

我的第一次尝试是遍历CSV并在每一行上执行断言。虽然这种方法有效,但我无法使用@Unroll将每个断言隔离到单独的独立测试中。

def "read from csv"() {
    expect:
    def reader = new CSVReader(...)
    def fields

    while ((fields = reader.readNext()) != null) {
        def firstNum = Integer.valueOf(fields[0])
        def secondNum = Integer.valueOf(fields[1])

        firstNum + 1 == secondNum
    }
}

尝试2

这种尝试允许我使用@Unroll,但这需要将整个数据加载到内存中,这是我首先想要避免的。

@Unroll
def "read from csv"() {
    expect:
    Integer.valueOf(firstNum as String) + 1 == Integer.valueOf(secondNum as String)

    where:
    [firstNum, secondNum] << new CSVReader(...).readAll()
}

尝试3

在阅读http://spock-framework.readthedocs.org/en/latest/data_driven_testing.html#data-pipes之后,我可以创建一个实现Iterable的对象...而Spock只会指示数据提供者只在需要时查询下一个值,这正是我想要。

@Unroll
def "read from csv"() {
    given:
    CSVParser csvParser = new CSVParser()

    expect:
    def fields = csvParser.parseLine(line as String)
    def firstNum = Integer.valueOf(fields[0])
    def secondNum = Integer.valueOf(fields[1])

    firstNum + 1 == secondNum

    where:
    line << new Iterable() {
        @Override
        Iterator iterator() {
            return new Scanner(...)
        }
    }
}

这种尝试并不算太糟糕,但我必须在expect块中进行一些CSV解析,这会使实际意图混乱,这就是执行断言,这看起来很奇怪。

尝试4

我的最后一次尝试几乎创建了一个迭代器包装器,它将字段作为单独的变量返回,但除非我将Iterable类提取到单独的API中,否则代码读起来相当难看。

@Unroll
def "read from csv"() {
    expect:
    Integer.valueOf(firstNum as String) + 1 == Integer.valueOf(secondNum as String)

    where:
    [firstNum, secondNum] << new Iterable() {
        @Override
        Iterator iterator() {
            new Iterator() {
                def reader = new CSVReader(...)

                def fields

                @Override
                boolean hasNext() {
                    fields = reader.readNext()
                    return fields != null
                }

                @Override
                Object next() {
                    return fields
                }

                @Override
                void remove() {
                    throw new UnsupportedOperationException()
                }
            }
        }
    }
}

问题

我的问题是......你将如何解决这个问题?有没有更好的方法(或更好的CSV库)?我知道Apache Commons CSV可能是我所知道的唯一一个实现Iterable的解析器,但它很长一段时间都是SNAPSHOT

非常感谢。

3 个答案:

答案 0 :(得分:5)

编写实用CSVFile(或Iterable<Iterable<String>>)的实用程序类Iterable<Iterable<Integer>>。然后使用where: [firstNum, secondNum] << new CSVFile("path/to/file")

答案 1 :(得分:2)

可能GroovyCSV会做你想要的事情:

  

GroovyCSV是一个使csv处理只是一点点Groovier的库。该库在幕后使用opencsv,只是尝试在混合中添加一层薄薄的“Groovy-ness”。

它是CsvParser方法返回迭代器。

答案 2 :(得分:1)

可能为时已晚,但我根据Peter Niederwieser的建议对此进行了编码。我会尝试将此提交给Spock。

它依赖于Guava和Apache Commons CSV库。在提交补丁之前,我会尝试删除这些依赖项。

import com.google.common.collect.Lists;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVRecord;

import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.Iterator;

/**
 * @author Aravind R Yarram
 * @version 1.0.0-SNAPSHOT
 * @since 1.0.0
 */
public class CSVFile implements Iterable<Iterable<String>>
{
    private final String fileName;

    public CSVFile(String fileName)
    {
        this.fileName = fileName;
    }

    /**
     * Returns an iterator over a set of elements of type T.
     *
     * @return an Iterator.
     */
    @Override public Iterator<Iterable<String>> iterator()
    {
        Iterable<CSVRecord> records = null;

        try
        {
            Reader in = new FileReader(fileName);
            records = CSVFormat.EXCEL.parse(in);
        }
        catch (IOException e)
        {
            throw new RuntimeException(e);
        }
        final Iterator<CSVRecord> it = records.iterator();

        return new Iterator<Iterable<String>>()
        {

            @Override public boolean hasNext()
            {
                return it.hasNext();
            }

            /**
             * Returns the next element in the iteration.
             *
             * @return the next element in the iteration
             * @throws NoSuchElementException if the iteration has no more elements
             */
            @Override public Iterable<String> next()
            {
                CSVRecord next = it.next();

                return Lists.newArrayList(next.iterator());
            }

            @Override public void remove()
            {
                throw new UnsupportedOperationException("Not supported");
            }
        };
    }
}