How to Test Messaging.sendEmail

You can use the Limits.getEmailInvocations method .

Test.startTest();
    methodThatSendsEmail();
    Integer invocations = Limits.getEmailInvocations();
Test.stopTest();

system.assertEquals(1, invocations, 'An email should be sent');

What to do about NO_MASS_MAIL_PERMISSION?

You can use the Messaging.sendEmail method with the optional allOrNone parameter set to false. To ignore the failures would be...unwise. I would create a custom object called Email_Log__c and save all results. I advise you to track anything you can set on the SingleEmailMessage class.

The below is a simple outline of how you can approach this problem.

public static final String DELIMITER = ';';
public static void safeSend(List<Messaging.SingleEmailMessage> emails)
{
    Integer index = 0;
    List<Email_Log__c> logs = new List<Email_Log__c>();
    for (Messaging.SendEmailResult result : Messaging.sendEmail(emails, false))
    {
        logs.add(buildLog(result, emails[index++]));
    }
    insert logs;
    // proper error handling omitted for brevity
}
static Email_Log__c buildLog
    (Messaging.SingleEmailMessage email, Messaging.SendEmailResult result)
{
    return new Email_Log__c(
        Is_Success__c = result.isSuccess(),
        Error_Messages__c = concatenateErrors(result.getErrors()),
        To_Addresses__c = String.join(email.getToAddresses(), DELIMITER),
        Html_Body__c = email.getHtmlBody()
        // other properties
    );
}
static String concatenateErrors(List<Messaging.SendEmailError> errors)
{
    List<String> messages = new List<String>();
    for (Messaging.SendEmailError error : errors) messages.add(error.getMessage());
    return String.join(messages, DELIMITER);
}

This approach is even more accessible via testing, because you can assert on the log objects created in addition to invocations. Even if every result is a failure, the invocation count augments by one.

system.assertEquals(1, invocations, 'An email should be sent');

List<Email_Log__c> logs = [SELECT To_Addresses__c, Etc__c FROM Email_Log__c];
system.assertEquals(numberOfEmailsSent, logs.size(), 'Each email should be sent');
for (Email_Log__c log : logs)
{
    // any additional assertions you desire
}

With respect to your edited question, as @AdrianLarson answered, your schedulable class still doesn't run until immediately after Test.stopTest(). Asserting the change in Limits.getEmailInvocations() should be sufficient for your requirements.

That being said, to answer the question you asked me on how to query the Messaging.SendEmailResult, that's something one executes immediately following their SendEmail Method (you'd do this in a non-async class). It isn't something one can query as those methods are run in the context of Sending an email. Querying them wouldn't have any context unless you queryied the WhoId, WhatId, sender, target, Subject, and other fields, etc to narrow the scope. In doing that you'd presumably be querying an Email record that's related to some Object Record rather than the SendEmailEmailResult method.

So, just add the code that @AdrianLarson provided to your test class and you should be all set. Additionally, you'll also want to assert that your scheduled class has run too. You already have the jobId.

After Test.stopTest(), add something like what's below:

 // Get the information from the CronTrigger API object
CronTrigger ct = [SELECT Id, CronExpression, TimesTriggered, 
     NextFireTime
     FROM CronTrigger WHERE id = :jobId];

// Verify the job has run
System.assertEquals(1, ct.TimesTriggered);

// Assert emails have been sent
system.assertEquals(1, invocations,  ' An email should be sent');

Edit

Your chron expression schedules the job for March 15th, 2022 at midnight which should work fine. Looking closer at your code, I see several things that could be the cause of your issues:

The first one is minor, but should be corrected. Use Schedulable, not System.Schedulable when you declare your class.

global class reportExporter implements System.Schedulable {

I suspect the real source of your problem is in this section of code below. Adding debug statements should reveal if that's the case. It would seem that either more of this code should be wrapped in if(!test.isRunningTest()) or else you've not created the data you need for it to run.

You begin by calling a page reference, creating the attachment, setting a file name for a blob, declare it's type, etc and actually attach it. It would seem to me that you'd need to create a blob in your test class called 'TEST BODY', but I don't see that in your test method.

    Messaging.EmailFileAttachment attachment = new Messaging.EmailFileAttachment();

    attachment.setFileName('Cost Plus Merchants.csv');

    //use getContent() only if not a test, otherwise test will fail
    //http://salesforce.stackexchange.com/questions/97223/test-fails-because-of-testmethod-do-not-support-getcontent-call
    if(!test.isRunningTest()){
        attachment.setBody(Blob.valueof(report.getContent().toString()));   
    } else {
        attachment.setBody(Blob.valueof('TEST BODY'));
    }

    attachment.setContentType('text/csv');

    Messaging.SingleEmailMessage message = new Messaging.SingleEmailMessage();

    message.setFileAttachments(new Messaging.EmailFileAttachment[] { attachment } );