Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This file outlines the requirements and best practices for adding new assertion

- **Version Compliance**: Always use the most current version of AGENTS.md and do not rely on cached or outdated copies. Refresh and re-read AGENTS.md before every change to ensure compliance with the latest requirements.
- **Branching and Commits**: It is forbidden to commit directly to the `main` branch. All changes must be added via pull request from a feature branch. If the current branch is `main`, MUST checkout to a new branch before changing any files. Do not push changes automatically—only push after explicit user request.
- **Pull Requests**: When creating a pull request, update the PR description with a detailed summary of changes, including new methods added, files modified, and any breaking changes. Ensure the description follows the format: Summary, Changes, Testing, Validation.
- **Method Signature**: All new methods must be public, accept an optional `$message` parameter (string, default empty), and return `self` to enable fluent chaining.
- **Type Safety**: Specify strict types for parameters where applicable (e.g., `int|float` for numeric comparisons). Avoid `mixed` unless necessary.
- **PHPUnit Integration**: Use appropriate PHPUnit assertion methods (e.g., `Assert::assertLessThan`) without named parameters for compatibility.
Expand Down
200 changes: 158 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,77 +18,193 @@ Write tests as usual, just use fluent assertions short aliases ``` check($x)->..

```php
// arrange
$user = UserFactory::createOne([
'phone' => $phoneBefore = faker()->e164PhoneNumber;
]);
$user = UserFactory::createOne();

// act
$user->setPhone(
$phoneAfter = faker()->e164PhoneNumber
);

// assert
$user->setPhone($e164PhoneNumber = faker()->e164PhoneNumber);

// traditional PHPUnit assertions
self::assertSame(expected: $phoneAfter, actual: $user->getPhone());
self::assertNotSame(expected: $phoneBefore, actual: $user->getPhone());
self::assertSame($e164PhoneNumber, $user->getPhone());

// fluent assertions
fact($user->getPhone())
->is($e164PhoneNumber)
->isString()
->startsWith('+7')
// etc.
;
```

### Comparison and Equality Methods
### Array assertions
```php
fact(42)->is(42)->equals(42)->not(43);
fact([1, 2, 3])->count(3); // Passes
fact([1, 2])->count(3); // Fails

fact([1, 2])->notCount(3); // Passes
fact([1, 2, 3])->notCount(3); // Fails

fact(['a' => ['b' => 'c']])->arrayContainsAssociativeArray(['a' => ['b' => 'c']]); // Passes
fact(['a' => ['b' => 'd']])->arrayContainsAssociativeArray(['a' => ['b' => 'c']]); // Fails

fact(['a' => 1])->arrayHasKey('a'); // Passes
fact(['a' => 1])->arrayHasKey('b'); // Fails

fact(['a' => 1])->arrayNotHasKey('b'); // Passes
fact(['a' => 1])->arrayNotHasKey('a'); // Fails

fact([1, 2, 3])->contains(2); // Passes
fact([1, 2])->contains(3); // Fails

fact([1, 2])->doesNotContain(3); // Passes
fact([1, 2, 3])->doesNotContain(3); // Fails

fact([1, 2])->hasSize(2); // Passes
fact([1, 2, 3])->hasSize(2); // Fails

fact([])->isEmptyArray(); // Passes
fact([1, 2])->isEmptyArray(); // Fails

fact([1, 2])->isNotEmptyArray(); // Passes
fact([])->isNotEmptyArray(); // Fails

```

### Boolean Methods
### Boolean assertions
```php
fact(true)->true()->notFalse();
fact(true)->true(); // Passes
fact(1)->true(); // Fails due to strict comparison

fact(false)->notTrue(); // Passes
fact(true)->notTrue(); // Fails

fact(false)->false(); // Passes
fact(0)->false(); // Fails due to strict comparison

fact(true)->notFalse(); // Passes
fact(false)->notFalse(); // Fails
```

### Null Methods

### Comparison and Equality assertions
```php
fact(null)->null()->notNull();
fact(42)->is(42); // Passes
fact(42)->is('42'); // Fails due to type difference

fact(42)->equals(42); // Passes
fact(42)->equals('42'); // Passes due to loose comparison

fact(42)->not(43); // Passes
fact(42)->not(42); // Fails
```

### Numeric Methods

### Null assertions
```php
fact(5)->isLowerThan(10)->isGreaterThan(0)->isPositive()->isNegative()->isZero()->isBetween(1, 10);
fact(null)->null(); // Passes
fact('')->null(); // Fails

fact(42)->notNull(); // Passes
fact(null)->notNull(); // Fails
```

### String Methods
### Numeric assertions
```php
fact('hello world')->matchesRegularExpression('/\w+/')->startsWith('hello')->endsWith('world')->hasLength(11)->isEmptyString();
fact(5)->isLowerThan(10); // Passes
fact(10)->isLowerThan(5); // Fails

fact(10)->isGreaterThan(5); // Passes
fact(5)->isGreaterThan(10); // Fails

fact(5)->isPositive(); // Passes
fact(-3)->isPositive(); // Fails

fact(-3)->isNegative(); // Passes
fact(5)->isNegative(); // Fails

fact(0)->isZero(); // Passes
fact(0.0)->isZero(); // Passes
fact(1)->isZero(); // Fails

fact(5)->isBetween(1, 10); // Passes
fact(15)->isBetween(1, 10); // Fails
```

### Array Methods
### Special assertions
```php
fact([1, 2, 3])->count(3)->contains(2)->doesNotContain(4)->hasSize(3)->isEmptyArray()->isNotEmptyArray();
fact('01ARZ3NDEKTSV4RRFFQ69G5FAV')->ulid(); // Passes (if valid ULID)
fact('invalid-ulid')->ulid(); // Fails
```

### Type Checking Methods
### String assertions
```php
fact(42)->isInt()->isString()->instanceOf(int::class)->hasProperty('name')->hasMethod('doSomething');
fact('abc123')->matchesRegularExpression('/^[a-z]+\d+$/'); // Passes
fact('123abc')->matchesRegularExpression('/^[a-z]+\d+$/'); // Fails

fact('123abc')->notMatchesRegularExpression('/^[a-z]+\d+$/'); // Passes
fact('abc123')->notMatchesRegularExpression('/^[a-z]+\d+$/'); // Fails

fact('hello world')->containsString('world'); // Passes
fact('hello world')->containsString('foo'); // Fails

fact('hello world')->notContainsString('foo'); // Passes
fact('hello world')->notContainsString('world'); // Fails

fact('Hello World')->containsStringIgnoringCase('world'); // Passes
fact('Hello World')->containsStringIgnoringCase('foo'); // Fails

fact('Hello World')->notContainsStringIgnoringCase('foo'); // Passes
fact('Hello World')->notContainsStringIgnoringCase('world'); // Fails

fact('hello world')->startsWith('hello'); // Passes
fact('world hello')->startsWith('hello'); // Fails

fact('file.txt')->endsWith('.txt'); // Passes
fact('txt.file')->endsWith('.txt'); // Fails

fact('abc')->hasLength(3); // Passes
fact('abcd')->hasLength(3); // Fails

fact('')->isEmptyString(); // Passes
fact('hello')->isEmptyString(); // Fails

fact('hello')->isNotEmptyString(); // Passes
fact('')->isNotEmptyString(); // Fails

fact('{"key": "value"}')->isJson(); // Passes
fact('invalid json')->isJson(); // Fails

fact('user@example.com')->isValidEmail(); // Passes
fact('invalid-email')->isValidEmail(); // Fails
```

### Special Methods
### Type Checking assertions
```php
fact('01ARZ3NDEKTSV4RRFFQ69G5FAV')->ulid();
```
...
;
fact(new stdClass())->instanceOf(stdClass::class); // Passes
fact(new stdClass())->instanceOf(Exception::class); // Fails

fact(new stdClass())->notInstanceOf(Exception::class); // Passes
fact(new stdClass())->notInstanceOf(stdClass::class); // Fails

fact(42)->isInt(); // Passes
fact('42')->isInt(); // Fails

fact('text')->isString(); // Passes
fact(42)->isString(); // Fails

fact((object)['name' => 'John'])->hasProperty('name'); // Passes
fact((object)['name' => 'John'])->hasProperty('age'); // Fails

fact(new stdClass())->hasMethod('__construct'); // Passes
fact(new stdClass())->hasMethod('nonExistentMethod'); // Fails

fact(3.14)->isFloat(); // Passes
fact(42)->isFloat(); // Fails

fact(
[
'a' => ['any' => 'thing'],
'b' => ['any' => 'thing', 'type' => 'candy', 'color' => 'green'],
'c' => ['miss' => 'kiss', 'foo' => 'bar', 'any' => 'thing'],
'd' => ['any' => 'thing'],
]
)->arrayContainsAssociativeArray(
[
'c' => ['foo' => 'bar', 'miss' => 'kiss'],
'b' => ['color' => 'green'],
]
); // true
fact(true)->isBool(); // Passes
fact(1)->isBool(); // Fails

fact([1, 2])->isArray(); // Passes
fact('not array')->isArray(); // Fails
```

## Pull requests are always welcome
Expand Down
76 changes: 68 additions & 8 deletions src/Traits/StringAssertions.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ trait StringAssertions
* fact('abc123')->matchesRegularExpression('/^[a-z]+\d+$/'); // Passes
* fact('123abc')->matchesRegularExpression('/^[a-z]+\d+$/'); // Fails
*
* @param non-empty-string $pattern The regular expression pattern to match against.
* @param string $pattern The regular expression pattern to match against.
* @param string $message Optional custom error message.
*
* @return self Enables fluent chaining of assertion methods.
Expand All @@ -48,7 +48,7 @@ public function matchesRegularExpression(string $pattern, string $message = ''):
* fact('123abc')->notMatchesRegularExpression('/^[a-z]+\d+$/'); // Passes
* fact('abc123')->notMatchesRegularExpression('/^[a-z]+\d+$/'); // Fails
*
* @param non-empty-string $pattern The regular expression pattern that should not match.
* @param string $pattern The regular expression pattern that should not match.
* @param string $message Optional custom error message.
*
* @return self Enables fluent chaining of assertion methods.
Expand Down Expand Up @@ -77,7 +77,7 @@ public function notMatchesRegularExpression(string $pattern, string $message = '
* fact('hello world')->containsString('world'); // Passes
* fact('hello world')->containsString('foo'); // Fails
*
* @param non-empty-string $string The substring to search for.
* @param string $string The substring to search for.
* @param string $message Optional custom error message.
*
* @return self Enables fluent chaining of assertion methods.
Expand All @@ -102,7 +102,7 @@ public function containsString(string $string, string $message = ''): self
* fact('hello world')->notContainsString('foo'); // Passes
* fact('hello world')->notContainsString('world'); // Fails
*
* @param non-empty-string $string The substring that should not be present.
* @param string $string The substring that should not be present.
* @param string $message Optional custom error message.
*
* @return self Enables fluent chaining of assertion methods.
Expand All @@ -127,7 +127,7 @@ public function notContainsString(string $string, string $message = ''): self
* fact('Hello World')->containsStringIgnoringCase('world'); // Passes
* fact('Hello World')->containsStringIgnoringCase('foo'); // Fails
*
* @param non-empty-string $string The substring to search for (case-insensitive).
* @param string $string The substring to search for (case-insensitive).
* @param string $message Optional custom error message.
*
* @return self Enables fluent chaining of assertion methods.
Expand All @@ -152,7 +152,7 @@ public function containsStringIgnoringCase(string $string, string $message = '')
* fact('Hello World')->notContainsStringIgnoringCase('foo'); // Passes
* fact('Hello World')->notContainsStringIgnoringCase('world'); // Fails
*
* @param non-empty-string $string The substring that should not be present (case-insensitive).
* @param string $string The substring that should not be present (case-insensitive).
* @param string $message Optional custom error message.
*
* @return self Enables fluent chaining of assertion methods.
Expand Down Expand Up @@ -185,7 +185,7 @@ public function notContainsStringIgnoringCase(string $string, string $message =
* fact('hello world')->startsWith('hello'); // Passes
* fact('world hello')->startsWith('hello'); // Fails
*
* @param non-empty-string $prefix The prefix to check for (must not be empty).
* @param string $prefix The prefix to check for (must not be empty).
* @param string $message Optional custom error message.
*
* @return self Enables fluent chaining of assertion methods.
Expand All @@ -211,7 +211,7 @@ public function startsWith(string $prefix, string $message = ''): self
* fact('file.txt')->endsWith('.txt'); // Passes
* fact('txt.file')->endsWith('.txt'); // Fails
*
* @param non-empty-string $suffix The suffix to check for (must not be empty).
* @param string $suffix The suffix to check for (must not be empty).
* @param string $message Optional custom error message.
*
* @return self Enables fluent chaining of assertion methods.
Expand Down Expand Up @@ -273,5 +273,65 @@ public function isEmptyString(string $message = ''): self
return $this;
}

/**
* Asserts that a string is not empty.
*
* This method checks if the actual value is not an empty string.
*
* Example usage:
* fact('hello')->isNotEmptyString(); // Passes
* fact('')->isNotEmptyString(); // Fails
*
* @param string $message Optional custom error message.
*
* @return self Enables fluent chaining of assertion methods.
*/
public function isNotEmptyString(string $message = ''): self
{
Assert::assertNotEmpty($this->variable, $message);

return $this;
}

/**
* Asserts that a string is valid JSON.
*
* This method checks if the actual string is valid JSON.
*
* Example usage:
* fact('{"key": "value"}')->isJson(); // Passes
* fact('invalid json')->isJson(); // Fails
*
* @param string $message Optional custom error message.
*
* @return self Enables fluent chaining of assertion methods.
*/
public function isJson(string $message = ''): self
{
Assert::assertTrue(json_validate($this->variable), $message ?: 'String is not valid JSON.');

return $this;
}

/**
* Asserts that a string is a valid email address.
*
* This method checks if the actual string is a valid email format.
*
* Example usage:
* fact('user@example.com')->isValidEmail(); // Passes
* fact('invalid-email')->isValidEmail(); // Fails
*
* @param string $message Optional custom error message.
*
* @return self Enables fluent chaining of assertion methods.
*/
public function isValidEmail(string $message = ''): self
{
Assert::assertTrue(filter_var($this->variable, FILTER_VALIDATE_EMAIL) !== false, $message ?: 'String is not a valid email address.');

return $this;
}

// endregion Length Methods
}
Loading