Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: flip expected and actual in std.testing.expectEqual #4437

Open
joachimschmidt557 opened this issue Feb 12, 2020 · 54 comments
Open
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. standard library This issue involves writing Zig code for the standard library.
Milestone

Comments

@joachimschmidt557
Copy link
Member

It may be useful to flip the arguments to std.testing.expectEqual. Right now, the expected value is the first formal parameter and the actual value is the second formal parameter.

When working with optional types, this makes testing via expectEqual impossible:

const std = @import("std");

pub fn expectEqual(actual: var, expected: @TypeOf(actual)) void {}

pub fn main() void {
    const x: ?u8 = 1;
    expectEqual(x, 1);

    // Doesn't work as the type of the expected value is comptime_int
    // std.testing.expectEqual(1, x);

    const y: ?u8 = null;
    expectEqual(x, null);

    // Doesn't work as the type of the expected value is (null)
    // std.testing.expectEqual(null, y);
}

But this may also be a very minor edge case.

@joachimschmidt557
Copy link
Member Author

An alternative would be to create a function in std.testing to handle optional types.

@daurnimator
Copy link
Contributor

daurnimator commented Feb 12, 2020

Right now, the expected value is the first formal parameter and the actual value is the second formal parameter.

This is expected so that actual can be coerced to expected. It's also a common pattern for testing frameworks, see e.g. http://olivinelabs.com/busted/#assert-equals

    const y: ?u8 = null;
    expectEqual(x, null);

    // Doesn't work as the type of the expected value is (null)
    // std.testing.expectEqual(null, y);

Write: td.testing.expectEqual(@as(?u8, null), y);

@daurnimator daurnimator added proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. standard library This issue involves writing Zig code for the standard library. labels Feb 12, 2020
@joachimschmidt557
Copy link
Member Author

Ok, when other testing frameworks (Junit, NUnit, etc.) also follow these guidelines, it should be ok to just explicitly cast the expected values to the correct type.

@fengb
Copy link
Contributor

fengb commented Feb 12, 2020

I actually favor this proposal. Adding those extra coercions is pretty gross and imo negatively affects readability. So either we're stuck with the correct but clunky expectEqual(@as(?u8, null), y) or we get by with the better looking but wrong expectEqual(y, null)

Alternately, we could see about coercing the literal to the other type if possible.

@mlarouche
Copy link
Contributor

I did helpers in my zigimg library to reserve the order of expected and actual because it annoyed me so much: https://github.com/mlarouche/zigimg/blob/master/tests/helpers.zig

@joachimschmidt557
Copy link
Member Author

I'll reopen this issue to because there seems some support for this.

@LemonBoy
Copy link
Contributor

#4438 proposes a different API that may solve this problem

@BarabasGitHub
Copy link
Contributor

BarabasGitHub commented Feb 13, 2020

I just noticed I do it the wrong way around in all my test code. 😐

Because I read it like this: expect x to be 1 and not expect 1 to be x I guess.

@andrewrk andrewrk added this to the 0.7.0 milestone Feb 13, 2020
@MageJohn
Copy link
Contributor

MageJohn commented Apr 8, 2020

It's not just with optional types this is annoying. For example:

const std = @import("std");

test "math" {
    const num: f64 = 1.5;
    std.testing.expectEqual(1.0, std.math.floor(num));
}

Which leads to this error:

./test.zig:5:48: error: expected type 'comptime_float', found 'f64'
    std.testing.expectEqual(1.0, std.math.floor(num));
                                               ^
./test.zig:5:28: note: referenced here
    std.testing.expectEqual(1.0, std.math.floor(num));
                           ^

As has been pointed out, this can be fixed with an explicit cast, which is ugly, or by swapping the arguments, which leads to the wrong error messages when a test fails.

Because testing is so easy to do in Zig, and because all the documentation uses tests to demonstrate concepts, new users are quite likely to use them. It may not be obvious to them that the correct solution is to cast the expected value. When I first encountered it, I "solved" the problem by swapping the arguments, which works to catch failing tests, but leads to confusing error messages.

@BarabasGitHub
Copy link
Contributor

Another solution would be to make the type explicit, so it would look like:

std.testing.expectEqual(f32, 1.0, std.math.floor(num));

@alinebee
Copy link

I ended up with this wrapper as a workaround, which preserves the intended parameter order and doesn't clutter the parameter list:

const testing = @import("std").testing;

pub fn expectEqual(expected: anytype, actual: anytype) void {
    testing.expectEqual(@as(@TypeOf(actual), expected), actual);
}

test "expectEqual correctly coerces types that std.testing.expectEqual does not" {
    const int_value: u8 = 2;
    expectEqual(2, int_value);

    const optional_value: ?u8 = null;
    expectEqual(null, optional_value);

    const Enum = enum { One, Two };
    const enum_value = Enum.One;
    expectEqual(.One, enum_value);
}

alinebee added a commit to alinebee/AZiggierWorld that referenced this issue Aug 29, 2020
The builtin `std.testing.expectEqual(expected, actual)` breaks type
inference for optionals and integers by forcing the `actual` value
to match the type of the `expected` value instead of vice-versa:
this causes code like `expectEqual(2, int_variable)` to fail to compile,
insisting that `int_variable` must be a `comptime_int` rather than
interpreting `2` as whatever type `int_variable` has.

Workarounds are to explicitly cast the expected value with
`@as(actual_type, expected_value)`, which is clunky, or to flip the
order of `actual` and `expected`, which causes misleading errors
on failed assertions.

This wrapper fixes this while preserving the intended parameter order.

See ziglang/zig#4437 (comment)
for further discussion.
@andrewrk andrewrk modified the milestones: 0.7.0, 0.8.0 Oct 27, 2020
@gonzus
Copy link
Contributor

gonzus commented Mar 4, 2021

FWIW, after working on several languages with good support for testing, my brain is hardwired for this:

const got = foo();
const expected = 11;
expect(got).toBe(expected);

@gwenzek
Copy link
Contributor

gwenzek commented Nov 20, 2021

I tried replacing the first line of expectEqual with the following:

pub fn expectEqual(expected_wrong_type: anytype, actual: anytype) !void {
    const expected = @as(@TypeOf(actual), expected_wrong_type);

It shows that actually a lot of standard library tests are using the expectEqual(actual, expected) to escape the current issue and will have to be reversed.
In this commit, you can see some examples that would need to be fixed.

https://github.com/gwenzek/zig/pull/2/files

I suppose there is also a lot of unit tests in the wild using flipped arguments, so we should try to fix this ASAP.
I can do the PR and fix the tests in STD lib, but I'd like to get confirmation this is the direction we want to go (since this is probably some significant amount of grunt work)

@Mouvedia
Copy link

another example: #10326 (comment)

@nektro
Copy link
Contributor

nektro commented Jan 18, 2022

adding a new function such as expectEql and deprecating expectEqual for a time would likely be the safest way forward, otherwise all compiles would still work yet error messages would be backwards. using a different function lets users know they need to update.

also I'm willing to take this on should it be accepted

@nektro
Copy link
Contributor

nektro commented Feb 4, 2022

I've also found multiple examples of people messing up the order in the wild given how unintuitive it is

@dvmason
Copy link

dvmason commented Mar 14, 2022

There are examples in the standard library of it being backward, too. e.g. std/mem.zig assumes it in places (like test "indexOfDiff")...

The natural thing is you type the value you're expecting, and then you say... hmm.. what should it be? All the other test infrastructures I've used do the (actual,expected) too.

That is also the right way for the type propagation to go... for all the DX reasons above (comptime ints and floats, optional values). In the rare case where the actual has the type wrong, many other things are going to flag it first (e.g. probably won't compile).

I don't see any need to create a new one and deprecate the old.... no working assertion would ever even see the difference in the error. Although an alias expectEquals would be nice as it reads more smoothly.

@andrewrk
Copy link
Member

andrewrk commented Mar 14, 2022

It's pretty clear that a big flaw is that the order is difficult to remember, and it's natural to put actual first when not referencing the function signature. But putting expected first makes sense so that actual can coerce to its type.

The problem pointed out in this issue does not seem like a real problem to me:

    // Doesn't work as the type of the expected value is comptime_int
    // std.testing.expectEqual(1, x);

You have to make the expected value have the correct type:

    std.testing.expectEqual(@as(?u8, 1), x);

Shouldn't you know what type you are expecting to find?

Similarly:

    std.testing.expectEqual(@as(?u8, null), y);

This function used to take actual: anytype as well and fail the check if @TypeOf(actual) != @TypeOf(expected). But I caved and let somebody change the signature to have actual coerce to the type of expected.

As for what we should do going forward, I do think something should change because as is people, including myself, occasionally get the order backwards. However I don't think simply flipping the order the other way around is a sufficient change to address the problem.

@nektro
Copy link
Contributor

nektro commented Apr 6, 2023

that is indeed the correct fix, but afaik the main issue of bikeshedding was if that should be the only change or if there should be a transition period of some sort so folks can fix their current usage and ensure its in the right order

@cztomsik
Copy link

cztomsik commented Apr 6, 2023

Yes, but in most cases, it would just mess up test reporting, and the test would fail anyway.

Also, as noted, I'm already using reversed order in some places, just because I find it way more readable than @as()

@codethief
Copy link

codethief commented Apr 6, 2023

@cztomsik

it would just mess up test reporting

"just". :)

Also, as noted, I'm already using reversed order in some places

I tried doing the same but the wrong order in the reporting kept tripping me up. So while I fully support @whatisaphone's proposal, I agree with @nektro here that some care needs to be taken when releasing this.

Maybe call the new version expectEquality & remove expectEqual, and later on (once everyone's removed expectEqual from their code base) rename expectEquality -> expectEqual?

@McSinyx
Copy link
Contributor

McSinyx commented Apr 7, 2023 via email

@dvmason
Copy link

dvmason commented Apr 9, 2023

I couldn't agree more...

Test reporting is wrong now for me, because I use this as actual,expected all the time, because the types just work, and it's more natural, and the way it works in many other unit-test environments.

It would be very nice if this got fixed sometime.

@codethief
Copy link

@McSinyx I am aware of that but one could still try to make migration as smooth as possible if it hardly costs anything.

@GrayHatter
Copy link
Contributor

When I try to call expectEqual(should_be, val_under_test) the build fails because val_under_test isn't comptime known.

src/tokenizer.zig: error: unable to resolve comptime value
    try expectEql(20, t.raw.len);
                      ~~~~~^~~~
src/tokenizer.zig: note: argument to parameter with comptime-only type must be comptime-known

(I doubt that's correct, perhaps another bug?) However if I reverse the params e.g. expectEqual(val_under_test, should_be) It builds and fails with a misleading error message. E.g.

const T = struct {
    raw: []const u8,
};

try expectEqual(t.raw.len, 20);

# zig build test
[...]
expected 21, found 20

While I agree with everyone above, expected_value should precede tested_value. I don't really care about the order. I do think, if you're checking types in tests, that the @TypeOf(expected) should be the cannon and asserted type. I.e. it shouldn't work when I write expectEqual(t.raw.len, 20); and fail to build when I follow the current naming expectEqual(20, t.raw.len); If 20 in this example was anything other than a literal comptime 20 I easily could have lost hours if I believed the given test error. It's near-trivial to fix my own bugs in tests, but what happens when it's someone else's?

An easy option would be to simplify the language of the error message to not use expected, or actual. Which might help punt until someone smarter than I am can come up with a better fix?

@DraagrenKirneh
Copy link
Contributor

DraagrenKirneh commented Apr 25, 2023

(I doubt that's correct, perhaps another bug?)

No that is the correct behavior, although the arrow may be misleading.
Any raw number ie const my_number = 20 without any type information has the type of comptime_int of integer values, and can be coerced to the correct value type ie const sum: usize = 40 + my_number if it can find the correct type information during compilation, but have limitations during runtime. The problem with expectEqual is that its the first parameter decides the type of the second one, which will give the error above as it expects t.raw.len to be of comptime_int instead of usize. This makes testing against constant numbers a bit of a hassle as you need to also specify the type of the value you are giving.
What I have ended up doing in my tests is to make a helper function:

fn expectEqual(comptime T: type, expected: T, actual: T) !void {
 return std.testing.expectEqual(expected, actual);
 }

So I can for example do: try expectEqual(usize, 20, t.raw.len);

@rofrol
Copy link
Contributor

rofrol commented Apr 26, 2023

@DraagrenKirneh

// reversed order, taken from zigimg
// I expect a + b to equal 42
pub fn expectEq(actual: anytype, expected: anytype) !void {
    try std.testing.expectEqual(@as(@TypeOf(actual), expected), actual);
}

@PastMoments
Copy link

Sorry for bringing more into the bikeshed, but I haven't seen anyone suggest invoking peer type resolution yet.
This way expected and actual will be coerced to whichever type they can both coerce into.
So the order of the parameters won't matter, it would behave similarly to == (which also invokes peer type resolution itself). It would also be backwards compatible as far as I know.

fn expectEqual(expected: anytype, actual: anytype) !void {
    const T = @TypeOf(expected, actual);
    const expected_val = @as(T, expected);
    const actual_val = @as(T, actual);
    // do comparisons with expected_val and actual_val
}

@AssortedFantasy
Copy link

I don't understand why you would ever want actual to coerce to expected.
It doesn't make sense for any of the following:

  • Anything you expect to be null.
  • Integer literals (comptime_int)
  • Floating point literals (comptime_float)
  • Struct literals and tuple literals
  • Enum literals

Or basically 99% of what you would be using in practice.
Sure if you look at JUnit or something its (expected, actual). But they don't do this nonsense coercion to go along with it. Both arguments are coerced to the same thing because of how overloading works.

Also I'd argue that it doesn't seem reasonable™️ to test types like this. Personally, I think tests should be written to test values and types should be tested when you compile your code and things compile because that's what type safety is and is a beautiful self updating test of sorts.

If you want to test types you should explicitly do testing.expectEqual(@TypeOf(foo), ?u8) or something similar because then it is way more clear what you intended was to test the type. Having it implicitly tested because your actually needs a coercion to go along with it seems weird.

@copygirl
Copy link

copygirl commented Sep 8, 2023

I just want to add my 2 cents here, as it's difficult to voice my opinion just through reactions to existing comments, even though others already have addressed the points I'm about to make.

I'm fine with the order of the parameters. I'm already used to it. Others can probably get used to it. Often, the actual parameter can take up the majority of a line visually in my editor, pushing the expected value I'm testing against out of view, which is often a simple primitive. I'm against @whatisaphone's case for swapping the parameters, as this will be very situational.

I think expected should coerce to actuals type. The type I'm testing is the result of actuals expression, often the return value of a function. If the return type itself is complicated (and for example calculated at comptime) and this needs to be tested directly, then the person writing the test can ensure the correct type is returned through more explicit means. Such tests would probably be so rare that it should not affect the common case to test equality.

Here are a couple cases in my test code where swapping what coerces to what would help with:

try expectEqual(@as(usize, 1), parts.len);
try expectEqual(@as(u32, 100), parts[0].id);
try expectEqual(@as(Path.EntityPart, .{ .id = 100 }), parts[0]);

try expectEqual(@as(?Entity(ctx), scope), world.getScope());
try expectEqual(scope, world.getScope().?); // I'm tempted to write this instead.
// And if you need to test against `null` you can do it using `@as`:
try expectEqual(@as(?Entity(ctx), null), world.getScope());
// Or I suppose one coud use `expect`:
try expect(world.getScope() == null);

This would be nicer to write:

try expectEqual(1, parts.len);
try expectEqual(100, parts[0].id);
try expectEqual(.{ .id = 100 }, parts[0]);

try expectEqual(scope, world.getScope());
try expectEqual(null, world.getScope());

And speaking of testing optionals, might it also be helpful to allow for expectEqualStrings to take optional or error union values?

// Very wordy to do the "correct" way?
const name = e.getName();
try expect(name != null);
try expectEqualStrings("child", name.?);
// I'm tempted to write this instead:
try expectEqualStrings("child", e.getName().?);
// When I wish I could do this:
// try expectEqualStrings("child", e.getName());

The @TypeOf(expected, actual) approach also sounds interesting but I don't know its full effects, and I'm not entirely if it's actually necessary in this case. (Besides for backwards compatibility reasons.)

@jasperdunn
Copy link

FWIW, after working on several languages with good support for testing, my brain is hardwired for this:

const got = foo();
const expected = 11;
expect(got).toBe(expected);

I know this is 2+ years ago, but I had fun learning about generics whilst playing with the idea 🤩

const std = @import("std");
const testing = std.testing;

fn Expect(comptime T: type) type {
    return struct {
        actual: T,

        const Self = @This();

        pub fn toBe(self: Self, expected: @TypeOf(self.actual)) !void {
            try testing.expectEqual(expected, self.actual);
        }
    };
}

pub fn expect(comptime T: type, actual: T) Expect(T) {
    return Expect(T){ .actual = actual };
}

test "expect(foo).toBe(foo)" {
    try expect(u8, 1).toBe(1);
    try expect(f16, 1.0).toBe(1.0);
    try expect(bool, true).toBe(true);
    try expect([]const u8, "test").toBe("test");
}

It would be nice not to need to write the type at the start, I'm sure that there is a way somehow...
Perhaps this kind of API could stop the confusion over what param goes first.

@Vexu
Copy link
Member

Vexu commented Nov 10, 2023

It would be nice not to need to write the type at the start, I'm sure that there is a way somehow...

Use anytype:

pub fn expect(actual: anytype) Expect(@TypeOf(actual)) {
    return Expect(@TypeOf(actual)){ .actual = actual };
}

test "expect(foo).toBe(foo)" {
    const x: ?u8 = 1;
    try expect(x).toBe(1);
}

@jasperdunn
Copy link

Use anytype:

I tried that, and got:

error: parameter of type 'testing.Expect(comptime\_int)' must be declared comptime  
pub fn toBe(self: Self, expected: T) !void {

After fixing those errors, I got:

error: the following command terminated unexpectedly

@Vexu
Copy link
Member

Vexu commented Nov 10, 2023

I wouldn't expect actual to be a number literal but you could add a helpful error for it:

pub fn expect(actual: anytype) Expect(@TypeOf(actual)) {
    switch (@TypeOf(actual)) {
        comptime_int, comptime_float => @compileError("actual is a number literal; did you mean to use it as the expected value?"),
        else => {}
    }
    return Expect(@TypeOf(actual)){ .actual = actual };
}

@Vexu
Copy link
Member

Vexu commented Jan 6, 2024

This could also be non-breaking for expect(<bool>) and with #18331 would also work for comptime only types:

pub fn Expect(comptime T: type) type {
    if (T == bool) return error{TestUnexpectedResult}!void;
    return struct {
        actual: T,

        pub inline fn equals(expected: @This(), actual: T) !void {
            return std.testing.expectEqualInner(T, expected.value, actual);
        }

        pub inline fn equalsError(expected: @This(), expected_error: anytype) !void {
            return std.testing.expectError(expected_error, expected.value);
        }
    };
}

pub fn expect(actual: anytype) Expect(@TypeOf(actual)) {
    if (@TypeOf(actual) == bool) {
        if (!actual) return error.TestUnexpectedResult;
        return;
    }
    return .{ .actual = actual };
}

fn foo(a: bool) !u32 {
    return if (a) 13 else error.Foo;
}

test expect {
    try expect(true);
    try expect(1).equals(1);
    try expect(foo(true)).equals(13);
    try expect(foo(false)).equalsError(error.Foo);
}

@nektro
Copy link
Contributor

nektro commented Jan 6, 2024

love that, and that it keeps the old code working during the transition!
nit: instead of .equals() the common nomenclature is .toBe()

@alinebee
Copy link

alinebee commented Jan 7, 2024

This could also be non-breaking for expect() and with #18331 would also work for comptime only types:

I find this appealingly clever, but I also feel that that style of test helper is not easy to write and extend in Zig.

For consistency, I'd feel compelled to fold the likes of expectFmt, expectEqualDeep, expectApproxEqualRel, etc. into the type returned by Expect as well. But many of these functions only apply to specific types of values (e.g. scalars vs non-scalars) and would need to be conditionally excluded from the returned type. Building a generic type with variable functions requires some fairly esoteric and unintuitive code in Zig, which would make Expect more complex to maintain and extend than the existing set of top-level expectBlah functions. Whereas leaving those more specialized assertions as top-level functions would introduce inconsistency and ambiguity into the Standard Library: at what point does an assertion helper deserve to be "promoted" to part of Expect rather than live alongside expect?

Similarly, it would leave users of Zig with an unsatisfactory choice for their own custom assertion helpers: do I continue to write top-level expectBlah functions which now feel inconsistent with the Standard Library conventions, or do I reimplement expect and Expect so that I can call my own helpers in a consistent style?

EDIT: While writing my followup below I realised that type-specific functions would not need to be conditionally excluded from the Expect struct as long as they are never called on an incompatible type.

@milanaleksic
Copy link

do I continue to write top-level expectBlah functions which now feel inconsistent with the Standard Library conventions

Coming from another background (Java), there's this super-popular testing library JUnit (I'm quite sure you heard about it). It had/has a similar problem: they introduced syntax assertThat(x, equalTo(y)) even though they had already top-level functions assertEquals(x,y). Community was not happy, but they/we understood the benefits of the new approach: composability, split if matchers' logic from assertions, etc. Today, I feel like the new approach is superior in almost any regard, even though older approach still has to be there for the backwards compatibility reasons.
I think Zig community could show a similar understanding and in time agree with the pattern change, what do you think?

@alinebee
Copy link

alinebee commented Jan 7, 2024

Community was not happy, but they/we understood the benefits of the new approach: composability, split if matchers' logic from assertions, etc. Today, I feel like the new approach is superior in almost any regard, even though older approach still has to be there for the backwards compatibility reasons.
I think Zig community could show a similar understanding and in time agree with the pattern change, what do you think?

In Zig, assertThat(x, equalTo(y)) would be composable, while expect(x).equals(y) would not. Code examples will make the problem easier to explain and the ergonomics easier to compare.

Let's say I want to assert that a value is contained within a slice (in practice I probably wouldn't bother to write a custom assertion function for this, but for the sake of argument!) With the status quo, I might write something like this:

const std = @import("std");

fn expectContains(haystack: anytype, needle: anytype) !void {
    if (std.mem.indexOfScalar(@TypeOf(needle), haystack, needle) == null) {
        return error.UnexpectedTestResult;
    }
}

test {
    const haystack = [_]usize{ 1, 2, 3 };
    const needle: usize = 2;
    try expectContains(&haystack, needle);
}

To phrase this as assertThat(haystack, contains(needle)) in the JUnit pattern, I would need to return a partially applied function or struct type instead:

const std = @import("std");

fn contains(needle: anytype) fn ([]const @TypeOf(needle)) bool {
    return struct {
        fn match(haystack: []const @TypeOf(needle)) bool {
            return std.mem.indexOfScalar(@TypeOf(needle), haystack, needle) != null;
        }
    }.match;
}

test {
    const haystack = [_]usize{ 1, 2, 3 };
    const needle: usize = 2;
    try std.testing.assertThat(haystack, contains(needle));
}

This is already more verbose and subtle than the status quo. As @milanaleksic suggests, it may make up for it through greater composability: e.g. in JUnit, letting you invert the logic with a not() predicate, chain with either().or(), reuse the same predicate functions to do a match-all/match-any within a slice, etc. (Though I'd argue that you can probably write helpers to do that with the status quo expectations already.)


To phrase this as expect(haystack).contains(needle), I would need to reimplement Expect to inject my contains function into the returned struct, then reimplement expect to return the new struct:

const std = @import("std");

pub fn ExpectWithCustomPredicates(comptime T: type) type {
    if (T == bool) return error{TestUnexpectedResult}!void;
    return struct {
        value: T,

        // The original predicates need to be duplicated in Zig 0.11.0;
        // you can't just borrow them with usingnamespace
        // because they'll have the wrong type for @This().
        pub fn equals(actual: @This(), expected: T) !void {
            return std.testing.expectEqual(expected, actual.value);
        }

        pub fn equalsError(actual: @This(), expected_error: anytype) !void {
            return std.testing.expectError(expected_error, actual.value);
        }
        
        pub fn contains(actual: @This(), needle: anytype) !void {
            if (std.mem.indexOfScalar(@TypeOf(needle), actual.value, needle) == null) {
                return error.UnexpectedTestResult;
            }
        }
    };
}

// The original expect function needs to be duplicated too,
// nothing can really be borrowed.
pub fn expect(actual: anytype) ExpectWithCustomPredicates(@TypeOf(actual)) {
    if (@TypeOf(actual) == bool) {
        if (!actual) return error.TestUnexpectedResult;
        return;
    }
    return .{ .value = value };
}

test {
    const haystack = [_]usize{ 1, 2, 3 };
    const needle: usize = 2;

    try expect(true);
    try expect(needle).equals(needle);
    try expect(&haystack).contains(needle);
}

This is much more verbose and subtle than the previous two examples, even if we assume some meta magic to avoid duplicating the original predicates; and it would be more complicated to write composable predicates like expect(&haystack).not().contains(needle). Without the ability to extend existing types, I feel this pattern goes against the grain of Zig.

@cztomsik
Copy link

cztomsik commented Jan 8, 2024

unsatisfactory choice for their own custom assertion helpers: do I continue to write top-level expectBlah

maybe I am alone but I like writing custom helpers anyway, I did not like it at first but more I do this the better the helpers are, and I can do things like errdefer in the while loop and log extra information when it fails, whereas with JUnit-style matchers this would be impossible (or incredibly complicated). tests are supposed to be simple (so you can be confident about them failing/passing).

really the only problem for me is flipping the order of arguments (or just swapping the coercion, I don't mind), because that @TypeOf requires a lot of @as() conversions.

@dominictobias
Copy link

dominictobias commented Nov 12, 2024

Ok, when other testing frameworks (Junit, NUnit, etc.) also follow these guidelines, it should be ok to just explicitly cast the expected values to the correct type.

Guess it depends if you are from the dusty world's of PHP, JVM or .NET, or the modern world's of Python/Rust/JS etc

JS:

https://jestjs.io/docs/expect#tobevalue
expect(can.ounces).toBe(12)

https://github.com/Automattic/expect.js
expect({ a: 'b' }).to.eql({ a: 'b' })

https://www.chaijs.com/
foo.should.equal('bar');

Rust:

https://doc.rust-lang.org/book/ch11-01-writing-tests.html

assert_eq!(result, 4);

Python:

https://www.dataquest.io/blog/unit-tests-python/

def test_sum(self):
        calculation = Calculations(8, 2)
        self.assertEqual(calculation.get_sum(), 10, 'The sum is wrong.')

To me this more intuitive and I work with Java/JUnit most days (sadly), but maybe because I've written too many Javascript tests first.

@RivenSkaye
Copy link

RivenSkaye commented Nov 28, 2024

To me this more intuitive and I work with Java/JUnit most days (sadly), but maybe because I've written too many Javascript tests first.

Having started with C#/.NET and Java back when I was studying, I find the Rust/Python/etc way of doing it to be much more logical. Though the JS ones seem... downright weird to be honest.

And as for Zig and the zen, code needs to read well. And to me, being able to say "I expect a + b to equal 42" feels a lot more natural than the other way around. And I quite like rofrol's/alinebee's earlier suggestion on implementing it as a way to not break backwards compatibility

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. standard library This issue involves writing Zig code for the standard library.
Projects
None yet
Development

No branches or pull requests