google / googletest

GoogleTest - Google Testing and Mocking Framework

Home Page:https://google.github.io/googletest/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Matchers for C++20 ranges

AndrewPratt0x40 opened this issue · comments

Does the feature exist in the most recent commit?

No

Why do we need this feature?

Motivation

Currently, it is not possible to test the values of C++20 ranges with matchers the same way that containers from the standard library can be tested. As more C++ applications use ranges throughout their public API it would be extremely helpful to have ranges supported by GoogleTest.

Previous related issues

Issues #3403 and #3564 touch on this, but both don't go as in-depth and only propose changing existing matchers instead of considering new ones.

Alternate Solutions

Wrapping ranges with a container

If the writer of a unit test wraps a C++20 ranges value in a standard library container, the existing matchers can be used the same way they are now.

Example

TEST(BandTests, BandHasAtLeastOneMember)
{
    const Band band{};
    auto ref_wrap_artist_fn = [](const Artist& artist){
        return std::ref(artist);
    }
    std::ranges::forward_range auto band_members_ref_range{
        band.band_members() | std::views::transform(ref_wrap_artist_fn)
    };
    std::vector<std::reference_wrapper<const Artist>> band_members_container {
#ifdef __cpp_lib_ranges_to_container
        band_members_ref_range | std::ranges::to<std::vector>
#else
        std::ranges::begin(band_members_ref_range),
        std::ranges::end(band_members_ref_range)
#endif
    };

    EXPECT_THAT(band_members_container, Not(IsEmpty());
}

Why not this?

While this works, it is far more verbose and requires more boilerplate to be written for every unit test that deals with a range. Additionally, the increased verbosity makes it less clear how exactly the range is being tested.

Calculating test values in separate statements

If the resulting value to test is stored in a variable ahead of time, a container matcher doesn't need to be used at all.

Example

TEST(BandTests, BandHasAtLeastOneMember)
{
    const Band band{};
    const bool band_members_empty{ std::ranges::empty(band.band_members()) };

    EXPECT_FALSE(band_members_empty);
}

Why not this?

While this works, it requires every unit test involving ranges to manually calculate whether the test should pass or not, which defeats the purpose of container matchers. Additionally, a failing test written this way would produce a far less helpful error message than a container matchers would as only the boolean result is used as opposed to the range being tested.

Custom (user-written) matchers

A test unit writer could make use of the MATCHER macros to define their own matchers for C++20 ranges.

Example

MATCHER(IsRangeEmpty, negation ? "empty" : "not empty") {
	return std::ranges::empty(arg);
}

TEST(BandTests, BandHasAtLeastOneMember)
{
    const Band band{};
    std::ranges::forward_range auto band_members{ band.band_members() };

    EXPECT_THAT(band_members, Not(IsRangeEmpty());
}

Why not this?

While this works, it requires the programmer to define a new corresponding matcher for every single container matcher themself. The user would then need to (or at least probably should) test each matcher to make sure they work correctly. These range-specific matchers may be frequently needed in a variety of C++20 applications and shouldn't require users to define them themselves for each project that uses them. Additionally, the C++20 ranges library is part of the standard library itself, and therefore is very likely to be used in user code.

Describe the proposal.

Summary

It would be nice if all of the currently existing container matchers either had corresponding matchers that supported C++20 ranges, or supported C++20 ranges themselves.

If changing the interface of the existing matchers would break existing code, new matchers that are specific to ranges could be defined instead. (In this context, all containers are ranges but not all ranges are containers.)

Separate overloads that take a start and end iterator could be defined as well, possibly for pre-C++20 code. This might not be necessary though.

To avoid introducing a breaking change, feature-test macros could be used internally for ranges-specific code.

Example

Here's an example of what this might look like:

// bands.h
#pragma once

#include <ranges>
#include <string>


struct Artist
{
    std::string name;
    bool is_lead_singer;
};

class Band
{
public:
    // Accessor method that returns a range of every band member
    std::ranges::forward_range auto band_members() const;

    // Accessor method that returns a read-only reference to the band's lead singer
    const Artist& lead_singer() const;

    // For simplicity just assume the constructor creates several default band members
    Band();

    /*...more code...*/
};


// bands_tests.cpp
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <ranges>
#include "bands.h"

TEST(BandTests, BandHasAtLeastOneMember)
{
    const Band band{};
    std::ranges::forward_range auto band_members{ band.band_members() };

    EXPECT_THAT(band_members, Not(IsEmpty());
    // OR: EXPECT_THAT(band_members, Not(IsEmptyRange());
}

TEST(BandTests, LeadSingerOfBandIsActuallyLeadSinger)
{
    const Band band{};

    EXPECT_TRUE(band.lead_singer().is_lead_singer);
}

TEST(BandTests, LeadSingerOfBandIsBandMember)
{
    const Band band{};
    std::ranges::forward_range auto band_members{ band.band_members() };
    const Artist& lead_singer{ band.lead_singer() };

    EXPECT_THAT(band_members, Contains(lead_singer));
    // OR: EXPECT_THAT(band_members, RangeContains(lead_singer));
}

TEST(BandTests, BandHasOnlyOneLeadSinger)
{
    const Band band{};
    auto is_lead_singer_fn = [](const Artist& artist){
        return artist.is_lead_singer;
    }
    std::ranges::input_range auto lead_singers {
        band.band_members()  | std::views::filter(is_lead_singer_fn)
    };

    EXPECT_THAT(lead_singers, SizeIs(1));
    // OR: EXPECT_THAT(lead_singers, RangeSizeIs(1));
}

Notes

  • I can, and would be happy to, attempt to implement some or all of the range matchers if that works best.
  • I have yet to sign the Contributor License Agreement (CLA) but can do so if needed.

Is the feature specific to an operating system, compiler, or build system version?

Changes should only be noticeable to C++ versions 20 and later.